remove class vue dep (1)

vue3
Varun Patil 2022-12-10 01:01:44 -08:00
parent 8520d0dc1e
commit 07379d836c
9 changed files with 2632 additions and 2577 deletions

View File

@ -38,7 +38,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Mixins, Watch } from "vue-property-decorator"; import Vue, { defineComponent } from "vue";
import NcContent from "@nextcloud/vue/dist/Components/NcContent"; import NcContent from "@nextcloud/vue/dist/Components/NcContent";
import NcAppContent from "@nextcloud/vue/dist/Components/NcAppContent"; import NcAppContent from "@nextcloud/vue/dist/Components/NcAppContent";
@ -49,15 +49,12 @@ const NcAppNavigationSettings = () =>
import("@nextcloud/vue/dist/Components/NcAppNavigationSettings"); import("@nextcloud/vue/dist/Components/NcAppNavigationSettings");
import { generateUrl } from "@nextcloud/router"; import { generateUrl } from "@nextcloud/router";
import { getCurrentUser } from "@nextcloud/auth";
import { translate as t } from "@nextcloud/l10n"; import { translate as t } from "@nextcloud/l10n";
import Timeline from "./components/Timeline.vue"; import Timeline from "./components/Timeline.vue";
import Settings from "./components/Settings.vue"; import Settings from "./components/Settings.vue";
import FirstStart from "./components/FirstStart.vue"; import FirstStart from "./components/FirstStart.vue";
import Metadata from "./components/Metadata.vue"; import Metadata from "./components/Metadata.vue";
import GlobalMixin from "./mixins/GlobalMixin";
import UserConfig from "./mixins/UserConfig";
import ImageMultiple from "vue-material-design-icons/ImageMultiple.vue"; import ImageMultiple from "vue-material-design-icons/ImageMultiple.vue";
import FolderIcon from "vue-material-design-icons/Folder.vue"; import FolderIcon from "vue-material-design-icons/Folder.vue";
@ -70,7 +67,8 @@ import PeopleIcon from "vue-material-design-icons/AccountBoxMultiple.vue";
import TagsIcon from "vue-material-design-icons/Tag.vue"; 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";
@Component({ export default defineComponent({
name: "App",
components: { components: {
NcContent, NcContent,
NcAppContent, NcAppContent,
@ -93,13 +91,109 @@ import MapIcon from "vue-material-design-icons/Map.vue";
TagsIcon, TagsIcon,
MapIcon, MapIcon,
}, },
data() {
return {
navItems: [],
metadataComponent: null as Metadata,
};
},
computed: {
ncVersion() {
const version = (<any>window.OC).config.version.split(".");
return Number(version[0]);
},
recognize() {
if (!this.config_recognizeEnabled) {
return false;
}
if (this.config_facerecognitionInstalled) {
return t("memories", "People (Recognize)");
}
return t("memories", "People");
},
facerecognition() {
if (!this.config_facerecognitionInstalled) {
return false;
}
if (this.config_recognizeEnabled) {
return t("memories", "People (Face Recognition)");
}
return t("memories", "People");
},
isFirstStart() {
return this.config_timelinePath === "EMPTY";
},
showAlbums() {
return this.config_albumsEnabled;
},
removeOuterGap() {
return this.ncVersion >= 25;
},
showNavigation() {
return this.$route.name !== "folder-share";
},
},
watch: {
route() {
this.doRouteChecks();
},
},
mounted() {
this.doRouteChecks();
// Populate navigation
this.navItems = this.navItemsAll().filter(
(item) => typeof item.if === "undefined" || Boolean(item.if)
);
// Store CSS variables modified
const root = document.documentElement;
const colorPrimary =
getComputedStyle(root).getPropertyValue("--color-primary");
root.style.setProperty("--color-primary-select-light", `${colorPrimary}40`);
root.style.setProperty("--plyr-color-main", colorPrimary);
// Register sidebar metadata tab
const OCA = globalThis.OCA;
if (OCA.Files && OCA.Files.Sidebar) {
OCA.Files.Sidebar.registerTab(
new OCA.Files.Sidebar.Tab({
id: "memories-metadata",
name: this.t("memories", "EXIF"),
icon: "icon-details",
async mount(el, fileInfo, context) {
if (this.metadataComponent) {
this.metadataComponent.$destroy();
}
this.metadataComponent = new Vue(Metadata);
// Only mount after we have all the info we need
await this.metadataComponent.update(fileInfo);
this.metadataComponent.$mount(el);
},
update(fileInfo) {
this.metadataComponent.update(fileInfo);
},
destroy() {
this.metadataComponent.$destroy();
this.metadataComponent = null;
},
}) })
export default class App extends Mixins(GlobalMixin, UserConfig) { );
// Outer element }
},
private metadataComponent!: Metadata; methods: {
navItemsAll() {
private readonly navItemsAll = (self: typeof this) => [ return [
{ {
name: "timeline", name: "timeline",
icon: ImageMultiple, icon: ImageMultiple,
@ -124,19 +218,19 @@ export default class App extends Mixins(GlobalMixin, UserConfig) {
name: "albums", name: "albums",
icon: AlbumIcon, icon: AlbumIcon,
title: t("memories", "Albums"), title: t("memories", "Albums"),
if: self.showAlbums, if: this.showAlbums,
}, },
{ {
name: "recognize", name: "recognize",
icon: PeopleIcon, icon: PeopleIcon,
title: self.recognize, title: this.recognize,
if: self.recognize, if: this.recognize,
}, },
{ {
name: "facerecognition", name: "facerecognition",
icon: PeopleIcon, icon: PeopleIcon,
title: self.facerecognition, title: this.facerecognition,
if: self.facerecognition, if: this.facerecognition,
}, },
{ {
name: "archive", name: "archive",
@ -152,115 +246,16 @@ export default class App extends Mixins(GlobalMixin, UserConfig) {
name: "tags", name: "tags",
icon: TagsIcon, icon: TagsIcon,
title: t("memories", "Tags"), title: t("memories", "Tags"),
if: self.config_tagsEnabled, if: this.config_tagsEnabled,
}, },
{ {
name: "maps", name: "maps",
icon: MapIcon, icon: MapIcon,
title: t("memories", "Maps"), title: t("memories", "Maps"),
if: self.config_mapsEnabled, if: this.config_mapsEnabled,
}, },
]; ];
private navItems = [];
get ncVersion() {
const version = (<any>window.OC).config.version.split(".");
return Number(version[0]);
}
get recognize() {
if (!this.config_recognizeEnabled) {
return false;
}
if (this.config_facerecognitionInstalled) {
return t("memories", "People (Recognize)");
}
return t("memories", "People");
}
get facerecognition() {
if (!this.config_facerecognitionInstalled) {
return false;
}
if (this.config_recognizeEnabled) {
return t("memories", "People (Face Recognition)");
}
return t("memories", "People");
}
get isFirstStart() {
return this.config_timelinePath === "EMPTY";
}
get showAlbums() {
return this.config_albumsEnabled;
}
get removeOuterGap() {
return this.ncVersion >= 25;
}
get showNavigation() {
return this.$route.name !== "folder-share";
}
@Watch("$route")
routeChanged() {
this.doRouteChecks();
}
mounted() {
this.doRouteChecks();
// Populate navigation
this.navItems = this.navItemsAll(this).filter(
(item) => typeof item.if === "undefined" || Boolean(item.if)
);
// Store CSS variables modified
const root = document.documentElement;
const colorPrimary =
getComputedStyle(root).getPropertyValue("--color-primary");
root.style.setProperty("--color-primary-select-light", `${colorPrimary}40`);
root.style.setProperty("--plyr-color-main", colorPrimary);
// Register sidebar metadata tab
const OCA = globalThis.OCA;
if (OCA.Files && OCA.Files.Sidebar) {
OCA.Files.Sidebar.registerTab(
new OCA.Files.Sidebar.Tab({
id: "memories-metadata",
name: this.t("memories", "EXIF"),
icon: "icon-details",
async mount(el, fileInfo, context) {
if (this.metadataComponent) {
this.metadataComponent.$destroy();
}
this.metadataComponent = new Metadata({
// Better integration with vue parent component
parent: context,
});
// Only mount after we have all the info we need
await this.metadataComponent.update(fileInfo);
this.metadataComponent.$mount(el);
}, },
update(fileInfo) {
this.metadataComponent.update(fileInfo);
},
destroy() {
this.metadataComponent.$destroy();
this.metadataComponent = null;
},
})
);
}
}
async beforeMount() { async beforeMount() {
if ("serviceWorker" in navigator) { if ("serviceWorker" in navigator) {
@ -279,18 +274,18 @@ export default class App extends Mixins(GlobalMixin, UserConfig) {
} else { } else {
console.debug("Service Worker is not enabled on this browser."); console.debug("Service Worker is not enabled on this browser.");
} }
} },
linkClick() { linkClick() {
const nav: any = this.$refs.nav; const nav: any = this.$refs.nav;
if (globalThis.windowInnerWidth <= 1024) nav?.toggleNavigation(false); if (globalThis.windowInnerWidth <= 1024) nav?.toggleNavigation(false);
} },
doRouteChecks() { doRouteChecks() {
if (this.$route.name === "folder-share") { if (this.$route.name === "folder-share") {
this.putFolderShareToken(this.$route.params.token); this.putFolderShareToken(this.$route.params.token);
} }
} },
putFolderShareToken(token: string) { putFolderShareToken(token: string) {
// Viewer looks for an input with ID sharingToken with the value as the token // Viewer looks for an input with ID sharingToken with the value as the token
@ -308,8 +303,9 @@ export default class App extends Mixins(GlobalMixin, UserConfig) {
} }
tokenInput.value = token; tokenInput.value = token;
} },
} },
});
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@ -47,7 +47,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Mixins } from "vue-property-decorator"; import { defineComponent } from "vue";
import NcContent from "@nextcloud/vue/dist/Components/NcContent"; import NcContent from "@nextcloud/vue/dist/Components/NcContent";
import NcAppContent from "@nextcloud/vue/dist/Components/NcAppContent"; import NcAppContent from "@nextcloud/vue/dist/Components/NcAppContent";
@ -57,37 +57,41 @@ import { getFilePickerBuilder } from "@nextcloud/dialogs";
import { getCurrentUser } from "@nextcloud/auth"; import { getCurrentUser } from "@nextcloud/auth";
import axios from "@nextcloud/axios"; import axios from "@nextcloud/axios";
import GlobalMixin from "../mixins/GlobalMixin";
import UserConfig from "../mixins/UserConfig";
import banner from "../assets/banner.svg"; import banner from "../assets/banner.svg";
import { IDay } from "../types"; import { IDay } from "../types";
import { API } from "../services/API"; import { API } from "../services/API";
@Component({ export default defineComponent({
name: "FirstStart",
components: { components: {
NcContent, NcContent,
NcAppContent, NcAppContent,
NcButton, NcButton,
}, },
})
export default class FirstStart extends Mixins(GlobalMixin, UserConfig) { data() {
banner = banner; return {
error = ""; banner,
info = ""; error: "",
show = false; info: "",
chosenPath = ""; show: false,
chosenPath: "",
};
},
mounted() { mounted() {
window.setTimeout(() => { window.setTimeout(() => {
this.show = true; this.show = true;
}, 300); }, 300);
} },
get isAdmin() { computed: {
isAdmin() {
return getCurrentUser().isAdmin; return getCurrentUser().isAdmin;
} },
},
methods: {
async begin() { async begin() {
const path = await this.chooseFolder( const path = await this.chooseFolder(
this.t("memories", "Choose the root of your timeline"), this.t("memories", "Choose the root of your timeline"),
@ -124,14 +128,14 @@ export default class FirstStart extends Mixins(GlobalMixin, UserConfig) {
} }
); );
this.chosenPath = path; this.chosenPath = path;
} },
async finish() { async finish() {
this.show = false; this.show = false;
await new Promise((resolve) => setTimeout(resolve, 500)); await new Promise((resolve) => setTimeout(resolve, 500));
this.config_timelinePath = this.chosenPath; this.config_timelinePath = this.chosenPath;
await this.updateSetting("timelinePath"); await this.updateSetting("timelinePath");
} },
async chooseFolder(title: string, initial: string) { async chooseFolder(title: string, initial: string) {
const picker = getFilePickerBuilder(title) const picker = getFilePickerBuilder(title)
@ -144,8 +148,9 @@ export default class FirstStart extends Mixins(GlobalMixin, UserConfig) {
.build(); .build();
return await picker.pick(); return await picker.pick();
} },
} },
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -45,8 +45,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Mixins } from "vue-property-decorator"; import { defineComponent } from "vue";
import GlobalMixin from "../mixins/GlobalMixin";
import NcActions from "@nextcloud/vue/dist/Components/NcActions"; import NcActions from "@nextcloud/vue/dist/Components/NcActions";
import NcActionButton from "@nextcloud/vue/dist/Components/NcActionButton"; import NcActionButton from "@nextcloud/vue/dist/Components/NcActionButton";
@ -68,53 +67,34 @@ 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 { API } from "../services/API"; import { API } from "../services/API";
@Component({ export default defineComponent({
name: "Metadata",
components: { components: {
NcActions, NcActions,
NcActionButton, NcActionButton,
EditIcon, EditIcon,
}, },
})
export default class Metadata extends Mixins(GlobalMixin) {
private fileInfo: IFileInfo = null;
private exif: { [prop: string]: any } = {};
private baseInfo: any = {};
private nominatim: any = null;
private state = 0;
public async update(fileInfo: IFileInfo) { data() {
this.state = Math.random(); return {
this.fileInfo = fileInfo; fileInfo: null as IFileInfo,
this.exif = {}; exif: {} as { [prop: string]: any },
this.nominatim = null; baseInfo: {} as any,
nominatim: null as any,
const state = this.state; state: 0,
const url = API.IMAGE_INFO(fileInfo.id); };
const res = await axios.get<any>(url); },
if (state !== this.state) return;
this.baseInfo = res.data;
this.exif = res.data.exif || {};
// Lazy loading
this.getNominatim().catch();
}
mounted() { mounted() {
subscribe("files:file:updated", this.handleFileUpdated); subscribe("files:file:updated", this.handleFileUpdated);
} },
beforeDestroy() { beforeDestroy() {
unsubscribe("files:file:updated", this.handleFileUpdated); unsubscribe("files:file:updated", this.handleFileUpdated);
} },
private handleFileUpdated({ fileid }) { computed: {
if (fileid && this.fileInfo?.id === fileid) { topFields() {
this.update(this.fileInfo);
}
}
get topFields() {
let list: { let list: {
title: string; title: string;
subtitle: string[]; subtitle: string[];
@ -169,10 +149,10 @@ export default class Metadata extends Mixins(GlobalMixin) {
} }
return list; return list;
} },
/** Date taken info */ /** Date taken info */
get dateOriginal() { dateOriginal() {
const dt = this.exif["DateTimeOriginal"] || this.exif["CreateDate"]; const dt = this.exif["DateTimeOriginal"] || this.exif["CreateDate"];
if (!dt) return null; if (!dt) return null;
@ -180,14 +160,14 @@ export default class Metadata extends Mixins(GlobalMixin) {
if (!m.isValid()) return null; if (!m.isValid()) return null;
m.locale(getCanonicalLocale()); m.locale(getCanonicalLocale());
return m; return m;
} },
get dateOriginalStr() { dateOriginalStr() {
if (!this.dateOriginal) return null; if (!this.dateOriginal) return null;
return utils.getLongDateStr(this.dateOriginal.toDate(), true); return utils.getLongDateStr(this.dateOriginal.toDate(), true);
} },
get dateOriginalTime() { dateOriginalTime() {
if (!this.dateOriginal) return null; if (!this.dateOriginal) return null;
// Try to get timezone // Try to get timezone
@ -199,18 +179,18 @@ export default class Metadata extends Mixins(GlobalMixin) {
if (tz) parts.push(tz); if (tz) parts.push(tz);
return parts; return parts;
} },
/** Camera make and model info */ /** Camera make and model info */
get camera() { camera() {
const make = this.exif["Make"]; const make = this.exif["Make"];
const model = this.exif["Model"]; const model = this.exif["Model"];
if (!make || !model) return null; if (!make || !model) return null;
if (model.startsWith(make)) return model; if (model.startsWith(make)) return model;
return `${make} ${model}`; return `${make} ${model}`;
} },
get cameraSub() { cameraSub() {
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"];
@ -222,10 +202,10 @@ export default class Metadata extends Mixins(GlobalMixin) {
if (len) parts.push(`${len}mm`); if (len) parts.push(`${len}mm`);
if (iso) parts.push(`ISO${iso}`); if (iso) parts.push(`ISO${iso}`);
return parts; return parts;
} },
/** Convert shutter speed decimal to 1/x format */ /** Convert shutter speed decimal to 1/x format */
get shutterSpeed() { shutterSpeed() {
const speed = Number( const speed = Number(
this.exif["ShutterSpeedValue"] || this.exif["ShutterSpeedValue"] ||
this.exif["ShutterSpeed"] || this.exif["ShutterSpeed"] ||
@ -238,14 +218,14 @@ export default class Metadata extends Mixins(GlobalMixin) {
} else { } else {
return `${Math.round(speed * 10) / 10}s`; return `${Math.round(speed * 10) / 10}s`;
} }
} },
/** Image info */ /** Image info */
get imageInfo() { imageInfo() {
return this.fileInfo.basename || (<any>this.fileInfo).name; return this.fileInfo.basename || (<any>this.fileInfo).name;
} },
get imageInfoSub() { imageInfoSub() {
let parts = []; let parts = [];
let mp = Number(this.exif["Megapixels"]); let mp = Number(this.exif["Megapixels"]);
@ -262,9 +242,9 @@ export default class Metadata extends Mixins(GlobalMixin) {
} }
return parts; return parts;
} },
get address() { address() {
if (!this.lat || !this.lon) return null; if (!this.lat || !this.lon) return null;
if (!this.nominatim) return this.t("memories", "Loading …"); if (!this.nominatim) return this.t("memories", "Loading …");
@ -281,17 +261,17 @@ export default class Metadata extends Mixins(GlobalMixin) {
} else { } else {
return n.display_name; return n.display_name;
} }
} },
get lat() { lat() {
return this.exif["GPSLatitude"]; return this.exif["GPSLatitude"];
} },
get lon() { lon() {
return this.exif["GPSLongitude"]; return this.exif["GPSLongitude"];
} },
get mapUrl() { mapUrl() {
const boxSize = 0.0075; const boxSize = 0.0075;
const bbox = [ const bbox = [
this.lon - boxSize, this.lon - boxSize,
@ -301,11 +281,37 @@ export default class Metadata extends Mixins(GlobalMixin) {
]; ];
const m = `${this.lat},${this.lon}`; const m = `${this.lat},${this.lon}`;
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}`;
} },
get mapFullUrl() { mapFullUrl() {
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}`;
},
},
methods: {
async update(fileInfo: IFileInfo) {
this.state = Math.random();
this.fileInfo = fileInfo;
this.exif = {};
this.nominatim = null;
const state = this.state;
const url = API.IMAGE_INFO(fileInfo.id);
const res = await axios.get<any>(url);
if (state !== this.state) return;
this.baseInfo = res.data;
this.exif = res.data.exif || {};
// Lazy loading
this.getNominatim().catch();
},
handleFileUpdated({ fileid }) {
if (fileid && this.fileInfo?.id === fileid) {
this.update(this.fileInfo);
} }
},
async getNominatim() { async getNominatim() {
const lat = this.lat; const lat = this.lat;
@ -318,8 +324,9 @@ export default class Metadata extends Mixins(GlobalMixin) {
); );
if (state !== this.state) return; if (state !== this.state) return;
this.nominatim = n.data; this.nominatim = n.data;
} },
} },
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -49,9 +49,8 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Mixins, Prop } from "vue-property-decorator"; import { defineComponent, PropType } from "vue";
import { IRow, IRowType, ITick } from "../types"; import { IRow, IRowType, ITick } from "../types";
import GlobalMixin from "../mixins/GlobalMixin";
import ScrollIcon from "vue-material-design-icons/UnfoldMoreHorizontal.vue"; import ScrollIcon from "vue-material-design-icons/UnfoldMoreHorizontal.vue";
import * as utils from "../services/Utils"; import * as utils from "../services/Utils";
@ -59,56 +58,63 @@ import * as utils from "../services/Utils";
// Pixels to snap at // Pixels to snap at
const SNAP_OFFSET = -35; const SNAP_OFFSET = -35;
@Component({ export default defineComponent({
name: "ScrollerManager",
components: { components: {
ScrollIcon, ScrollIcon,
}, },
})
export default class ScrollerManager extends Mixins(GlobalMixin) { props: {
/** Rows from Timeline */ /** Rows from Timeline */
@Prop() rows!: IRow[]; rows: Array as PropType<IRow[]>,
/** Total height */ /** Total height */
@Prop() height!: number; height: Number,
/** Actual recycler component */ /** Actual recycler component */
@Prop() recycler!: any; recycler: Object,
/** Recycler before slot component */ /** Recycler before slot component */
@Prop() recyclerBefore!: any; recyclerBefore: HTMLDivElement,
},
data() {
return {
/** Last known height at adjustment */ /** Last known height at adjustment */
private lastAdjustHeight = 0; lastAdjustHeight: 0,
/** Height of the entire photo view */ /** Height of the entire photo view */
private recyclerHeight: number = 100; recyclerHeight: 100,
/** Rect of scroller */ /** Rect of scroller */
private scrollerRect: DOMRect = null; scrollerRect: null as DOMRect,
/** Computed ticks */ /** Computed ticks */
private ticks: ITick[] = []; ticks: [] as ITick[],
/** Computed cursor top */ /** Computed cursor top */
private cursorY = 0; cursorY: 0,
/** Hover cursor top */ /** Hover cursor top */
private hoverCursorY = -5; hoverCursorY: -5,
/** Hover cursor text */ /** Hover cursor text */
private hoverCursorText = ""; hoverCursorText: "",
/** Scrolling using the scroller */ /** Scrolling using the scroller */
private scrollingTimer = 0; scrollingTimer: 0,
/** Scrolling now using the scroller */ /** Scrolling now using the scroller */
private scrollingNowTimer = 0; scrollingNowTimer: 0,
/** Scrolling recycler */ /** Scrolling recycler */
private scrollingRecyclerTimer = 0; scrollingRecyclerTimer: 0,
/** Scrolling recycler now */ /** Scrolling recycler now */
private scrollingRecyclerNowTimer = 0; scrollingRecyclerNowTimer: 0,
/** Recycler scrolling throttle */ /** Recycler scrolling throttle */
private scrollingRecyclerUpdateTimer = 0; scrollingRecyclerUpdateTimer: 0,
/** View size reflow timer */ /** View size reflow timer */
private reflowRequest = false; reflowRequest: false,
/** Tick adjust timer */ /** Tick adjust timer */
private adjustRequest = false; adjustRequest: false,
/** Scroller is being moved with interaction */ /** Scroller is being moved with interaction */
private interacting = false; interacting: false,
/** Track the last requested y position when interacting */ /** Track the last requested y position when interacting */
private lastRequestedRecyclerY = 0; lastRequestedRecyclerY: 0,
};
},
computed: {
/** Get the visible ticks */ /** Get the visible ticks */
get visibleTicks() { visibleTicks() {
let key = 9999999900; let key = 9999999900;
return this.ticks return this.ticks
.filter((tick) => tick.s) .filter((tick) => tick.s)
@ -120,10 +126,12 @@ export default class ScrollerManager extends Mixins(GlobalMixin) {
} }
return tick; return tick;
}); });
} },
},
methods: {
/** Reset state */ /** Reset state */
public reset() { reset() {
this.ticks = []; this.ticks = [];
this.cursorY = 0; this.cursorY = 0;
this.hoverCursorY = -5; this.hoverCursorY = -5;
@ -141,10 +149,10 @@ export default class ScrollerManager extends Mixins(GlobalMixin) {
this.scrollingRecyclerTimer = 0; this.scrollingRecyclerTimer = 0;
this.scrollingRecyclerNowTimer = 0; this.scrollingRecyclerNowTimer = 0;
this.scrollingRecyclerUpdateTimer = 0; this.scrollingRecyclerUpdateTimer = 0;
} },
/** Recycler scroll event, must be called by timeline */ /** Recycler scroll event, must be called by timeline */
public recyclerScrolled() { recyclerScrolled() {
// This isn't a renewing timer, it's a scheduled task // This isn't a renewing timer, it's a scheduled task
if (this.scrollingRecyclerUpdateTimer) return; if (this.scrollingRecyclerUpdateTimer) return;
this.scrollingRecyclerUpdateTimer = window.setTimeout(() => { this.scrollingRecyclerUpdateTimer = window.setTimeout(() => {
@ -155,10 +163,10 @@ export default class ScrollerManager extends Mixins(GlobalMixin) {
// Update that we're scrolling with the recycler // Update that we're scrolling with the recycler
utils.setRenewingTimeout(this, "scrollingRecyclerNowTimer", null, 200); utils.setRenewingTimeout(this, "scrollingRecyclerNowTimer", null, 200);
utils.setRenewingTimeout(this, "scrollingRecyclerTimer", null, 1500); utils.setRenewingTimeout(this, "scrollingRecyclerTimer", null, 1500);
} },
/** Update cursor position from recycler scroll position */ /** Update cursor position from recycler scroll position */
public updateFromRecyclerScroll() { updateFromRecyclerScroll() {
// Ignore if not initialized or moving // Ignore if not initialized or moving
if (!this.ticks.length || this.interacting) return; if (!this.ticks.length || this.interacting) return;
@ -180,19 +188,19 @@ export default class ScrollerManager extends Mixins(GlobalMixin) {
} else { } else {
this.moveHoverCursor(rtop); this.moveHoverCursor(rtop);
} }
} },
/** Re-create tick data in the next frame */ /** Re-create tick data in the next frame */
public async reflow() { async reflow() {
if (this.reflowRequest) return; if (this.reflowRequest) return;
this.reflowRequest = true; this.reflowRequest = true;
await this.$nextTick(); await this.$nextTick();
this.reflowNow(); this.reflowNow();
this.reflowRequest = false; this.reflowRequest = false;
} },
/** Re-create tick data */ /** Re-create tick data */
private reflowNow() { reflowNow() {
// Ignore if not initialized // Ignore if not initialized
if (!this.recycler?.$refs.wrapper) return; if (!this.recycler?.$refs.wrapper) return;
@ -204,10 +212,10 @@ export default class ScrollerManager extends Mixins(GlobalMixin) {
// Adjust top // Adjust top
this.adjustNow(); this.adjustNow();
} },
/** Recreate from scratch */ /** Recreate from scratch */
private recreate() { recreate() {
// Clear and override any adjust timer // Clear and override any adjust timer
this.ticks = []; this.ticks = [];
@ -256,22 +264,22 @@ export default class ScrollerManager extends Mixins(GlobalMixin) {
} }
} }
} }
} },
/** /**
* Update tick positions without truncating the list * Update tick positions without truncating the list
* This is much cheaper than reflowing the whole thing * This is much cheaper than reflowing the whole thing
*/ */
public async adjust() { async adjust() {
if (this.adjustRequest) return; if (this.adjustRequest) return;
this.adjustRequest = true; this.adjustRequest = true;
await this.$nextTick(); await this.$nextTick();
this.adjustNow(); this.adjustNow();
this.adjustRequest = false; this.adjustRequest = false;
} },
/** Do adjustment synchronously */ /** Do adjustment synchronously */
private adjustNow() { adjustNow() {
// Refresh height of recycler // Refresh height of recycler
this.recyclerHeight = this.recycler.$refs.wrapper.clientHeight; this.recyclerHeight = this.recycler.$refs.wrapper.clientHeight;
const extraY = this.recyclerBefore?.clientHeight || 0; const extraY = this.recyclerBefore?.clientHeight || 0;
@ -319,10 +327,10 @@ export default class ScrollerManager extends Mixins(GlobalMixin) {
this.setTicksTop(count); this.setTicksTop(count);
this.computeVisibleTicks(); this.computeVisibleTicks();
} }
} },
/** Mark ticks as visible or invisible */ /** Mark ticks as visible or invisible */
private computeVisibleTicks() { computeVisibleTicks() {
// Kind of unrelated here, but refresh rect // Kind of unrelated here, but refresh rect
this.scrollerRect = ( this.scrollerRect = (
this.$refs.scroller as HTMLElement this.$refs.scroller as HTMLElement
@ -381,17 +389,17 @@ export default class ScrollerManager extends Mixins(GlobalMixin) {
tick.s = true; tick.s = true;
prevShow = tick.top; prevShow = tick.top;
} }
} },
private setTicksTop(total: number) { setTicksTop(total: number) {
for (const tick of this.ticks) { for (const tick of this.ticks) {
tick.topF = this.height * (tick.count / total); tick.topF = this.height * (tick.count / total);
tick.top = utils.roundHalf(tick.topF); tick.top = utils.roundHalf(tick.topF);
} }
} },
/** Change actual position of the hover cursor */ /** Change actual position of the hover cursor */
private moveHoverCursor(y: number) { moveHoverCursor(y: number) {
this.hoverCursorY = y; this.hoverCursorY = y;
// Get index of previous tick // Get index of previous tick
@ -415,24 +423,24 @@ export default class ScrollerManager extends Mixins(GlobalMixin) {
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 */
private mousemove(event: MouseEvent) { mousemove(event: MouseEvent) {
if (event.buttons) { if (event.buttons) {
this.mousedown(event); this.mousedown(event);
} }
this.moveHoverCursor(event.offsetY); this.moveHoverCursor(event.offsetY);
} },
/** Handle mouse leave */ /** Handle mouse leave */
private mouseleave() { mouseleave() {
this.interactend(); this.interactend();
this.moveHoverCursor(this.cursorY); this.moveHoverCursor(this.cursorY);
} },
/** Binary search and get coords surrounding position */ /** Binary search and get coords surrounding position */
private getCoords(y: number, field: "topF" | "y") { getCoords(y: number, field: "topF" | "y") {
// Top of first and second ticks // Top of first and second ticks
let top1 = 0, let top1 = 0,
top2 = 0, top2 = 0,
@ -462,10 +470,10 @@ export default class ScrollerManager extends Mixins(GlobalMixin) {
} }
return { top1, top2, y1, y2 }; return { top1, top2, y1, y2 };
} },
/** Move to given scroller Y */ /** Move to given scroller Y */
private moveto(y: number, snap: boolean) { moveto(y: number, snap: boolean) {
// Move cursor immediately to prevent jank // Move cursor immediately to prevent jank
this.cursorY = y; this.cursorY = y;
this.hoverCursorY = y; this.hoverCursorY = y;
@ -481,36 +489,37 @@ export default class ScrollerManager extends Mixins(GlobalMixin) {
} }
this.handleScroll(); this.handleScroll();
} },
/** Handle mouse click */ /** Handle mouse click */
private mousedown(event: MouseEvent) { mousedown(event: MouseEvent) {
this.interactstart(); // end called on mouseup this.interactstart(); // end called on mouseup
this.moveto(event.offsetY, false); this.moveto(event.offsetY, false);
} },
/** Handle touch */ /** Handle touch */
private touchmove(event: any) { touchmove(event: any) {
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);
} },
private interactstart() { interactstart() {
this.interacting = true; this.interacting = true;
} },
private interactend() { interactend() {
this.interacting = false; this.interacting = false;
this.recyclerScrolled(); // make sure final position is correct this.recyclerScrolled(); // make sure final position is correct
} },
/** Update scroller is being used to scroll recycler */ /** Update scroller is being used to scroll recycler */
private handleScroll() { handleScroll() {
utils.setRenewingTimeout(this, "scrollingNowTimer", null, 200); utils.setRenewingTimeout(this, "scrollingNowTimer", null, 200);
utils.setRenewingTimeout(this, "scrollingTimer", null, 1500); utils.setRenewingTimeout(this, "scrollingTimer", null, 1500);
} },
} },
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -48,9 +48,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Emit, Mixins, Prop, Watch } from "vue-property-decorator"; import { defineComponent, PropType } from "vue";
import GlobalMixin from "../mixins/GlobalMixin";
import UserConfig from "../mixins/UserConfig";
import { showError } from "@nextcloud/dialogs"; import { showError } from "@nextcloud/dialogs";
@ -91,7 +89,8 @@ import AlbumRemoveIcon from "vue-material-design-icons/BookRemove.vue";
type Selection = Map<number, IPhoto>; type Selection = Map<number, IPhoto>;
@Component({ export default defineComponent({
name: "SelectionManager",
components: { components: {
NcActions, NcActions,
NcActionButton, NcActionButton,
@ -102,46 +101,35 @@ type Selection = Map<number, IPhoto>;
CloseIcon, CloseIcon,
}, },
})
export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
@Prop() public heads: { [dayid: number]: IHeadRow };
props: {
heads: Object as PropType<{ [dayid: number]: IHeadRow }>,
/** List of rows for multi selection */ /** List of rows for multi selection */
@Prop() public rows: IRow[]; rows: Array as PropType<IRow[]>,
/** Rows are in ascending order (desc is normal) */ /** Rows are in ascending order (desc is normal) */
@Prop() public isreverse: boolean; isreverse: Boolean,
/** Recycler element to scroll during touch multi-select */ /** Recycler element to scroll during touch multi-select */
@Prop() public recycler: any; recycler: Object,
},
private show = false; data() {
private size = 0; return {
private readonly selection!: Selection; show: false,
private readonly defaultActions: ISelectionAction[]; size: 0,
selection: new Map<number, IPhoto>(),
defaultActions: null as ISelectionAction[],
private touchAnchor: IPhoto = null; touchAnchor: null as IPhoto,
private touchTimer: number = 0; touchTimer: 0,
private touchPrevSel!: Selection; touchPrevSel: null as Selection,
private prevOver!: IPhoto; prevOver: null as IPhoto,
private touchScrollInterval: number = 0; touchScrollInterval: 0,
private touchScrollDelta: number = 0; touchScrollDelta: 0,
private prevTouch: Touch = null; prevTouch: null as Touch,
};
@Emit("refresh") },
refresh() {}
@Emit("delete")
deletePhotos(photos: IPhoto[]) {}
@Emit("updateLoading")
updateLoading(delta: number) {}
constructor() {
super();
this.selection = new Map<number, IPhoto>();
mounted() {
// Make default actions // Make default actions
this.defaultActions = [ this.defaultActions = [
{ {
@ -230,66 +218,83 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
this.editDateSelection(getSel(photo)); this.editDateSelection(getSel(photo));
globalThis.editExif = (photo: IPhoto) => globalThis.editExif = (photo: IPhoto) =>
this.editExifSelection(getSel(photo)); this.editExifSelection(getSel(photo));
} },
/** Download is not allowed on some public shares */ watch: {
private allowDownload(): boolean { show() {
return this.state_noDownload;
}
/** Archive is not allowed only on folder routes */
private allowArchive() {
return this.$route.name !== "folders";
}
/** Is archive route */
private routeIsArchive() {
return this.$route.name === "archive";
}
/** Is album route */
private routeIsAlbum() {
return this.config_albumsEnabled && this.$route.name === "albums";
}
/** Public route that can't modify anything */
private routeIsPublic() {
return this.$route.name === "folder-share";
}
@Watch("show")
onShowChange() {
const klass = "has-top-bar"; const klass = "has-top-bar";
if (this.show) { if (this.show) {
document.body.classList.add(klass); document.body.classList.add(klass);
} else { } else {
document.body.classList.remove(klass); document.body.classList.remove(klass);
} }
} },
},
methods: {
refresh() {
this.$emit("refresh");
},
deletePhotos(photos: IPhoto[]) {
this.$emit("deletePhotos", photos);
},
updateLoading(delta: number) {
this.$emit("updateLoading", delta);
},
/** Download is not allowed on some public shares */
allowDownload(): boolean {
return this.state_noDownload;
},
/** Archive is not allowed only on folder routes */
allowArchive() {
return this.$route.name !== "folders";
},
/** Is archive route */
routeIsArchive() {
return this.$route.name === "archive";
},
/** Is album route */
routeIsAlbum() {
return this.config_albumsEnabled && this.$route.name === "albums";
},
/** Public route that can't modify anything */
routeIsPublic() {
return this.$route.name === "folder-share";
},
/** Trigger to update props from selection set */ /** Trigger to update props from selection set */
private selectionChanged() { selectionChanged() {
this.show = this.selection.size > 0; this.show = this.selection.size > 0;
this.size = this.selection.size; this.size = this.selection.size;
} },
/** Is this fileid (or anything if not specified) selected */ /** Is this fileid (or anything if not specified) selected */
public has(fileid?: number) { has(fileid?: number) {
if (fileid === undefined) { if (fileid === undefined) {
return this.selection.size > 0; return this.selection.size > 0;
} }
return this.selection.has(fileid); return this.selection.has(fileid);
} },
/** Get the actions list */ /** Get the actions list */
private getActions(): ISelectionAction[] { getActions(): ISelectionAction[] {
return this.defaultActions.filter( return (
(a) => (!a.if || a.if(this)) && (!this.routeIsPublic() || a.allowPublic) this.defaultActions?.filter(
(a) =>
(!a.if || a.if(this)) && (!this.routeIsPublic() || a.allowPublic)
) || []
); );
} },
/** Click on an action */ /** Click on an action */
private async click(action: ISelectionAction) { async click(action: ISelectionAction) {
try { try {
this.updateLoading(1); this.updateLoading(1);
await action.callback(this.selection); await action.callback(this.selection);
@ -298,10 +303,10 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
} finally { } finally {
this.updateLoading(-1); this.updateLoading(-1);
} }
} },
/** Clicking on photo */ /** Clicking on photo */
public clickPhoto(photo: IPhoto, event: PointerEvent, rowIdx: number) { clickPhoto(photo: IPhoto, event: PointerEvent, rowIdx: number) {
if (photo.flag & this.c.FLAG_PLACEHOLDER) return; if (photo.flag & this.c.FLAG_PLACEHOLDER) return;
if (event.pointerType === "touch") return; // let touch events handle this if (event.pointerType === "touch") return; // let touch events handle this
@ -314,10 +319,10 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
} else { } else {
this.openViewer(photo); this.openViewer(photo);
} }
} },
/** Tap on */ /** Tap on */
protected touchstartPhoto(photo: IPhoto, event: TouchEvent, rowIdx: number) { touchstartPhoto(photo: IPhoto, event: TouchEvent, rowIdx: number) {
if (photo.flag & this.c.FLAG_PLACEHOLDER) return; if (photo.flag & this.c.FLAG_PLACEHOLDER) return;
this.rows[rowIdx].virtualSticky = true; this.rows[rowIdx].virtualSticky = true;
@ -332,18 +337,18 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
} }
this.touchTimer = 0; this.touchTimer = 0;
}, 600); }, 600);
} },
/** Tap off */ /** Tap off */
protected touchendPhoto(photo: IPhoto, event: TouchEvent, rowIdx: number) { touchendPhoto(photo: IPhoto, event: TouchEvent, rowIdx: number) {
if (photo.flag & this.c.FLAG_PLACEHOLDER) return; if (photo.flag & this.c.FLAG_PLACEHOLDER) return;
delete this.rows[rowIdx].virtualSticky; delete this.rows[rowIdx].virtualSticky;
if (this.touchTimer) this.clickPhoto(photo, {} as any, rowIdx); if (this.touchTimer) this.clickPhoto(photo, {} as any, rowIdx);
this.resetTouchParams(); this.resetTouchParams();
} },
private resetTouchParams() { resetTouchParams() {
window.clearTimeout(this.touchTimer); window.clearTimeout(this.touchTimer);
this.touchTimer = 0; this.touchTimer = 0;
this.touchAnchor = null; this.touchAnchor = null;
@ -353,13 +358,13 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
this.touchScrollInterval = 0; this.touchScrollInterval = 0;
this.prevTouch = null; this.prevTouch = null;
} },
/** /**
* Tap over * Tap over
* photo and rowIdx are that of the *anchor* * photo and rowIdx are that of the *anchor*
*/ */
protected touchmovePhoto(anchor: IPhoto, event: TouchEvent, rowIdx: number) { touchmovePhoto(anchor: IPhoto, event: TouchEvent, rowIdx: number) {
if (anchor.flag & this.c.FLAG_PLACEHOLDER) return; if (anchor.flag & this.c.FLAG_PLACEHOLDER) return;
if (this.touchTimer) { if (this.touchTimer) {
@ -415,15 +420,16 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
} }
this.touchMoveSelect(touch, rowIdx); this.touchMoveSelect(touch, rowIdx);
} },
/** Multi-select triggered by touchmove */ /** Multi-select triggered by touchmove */
private touchMoveSelect(touch: Touch, rowIdx: number) { touchMoveSelect(touch: Touch, rowIdx: number) {
// Which photo is the cursor over, if any // Which photo is the cursor over, if any
const elems = document.elementsFromPoint(touch.clientX, touch.clientY); const elems = document.elementsFromPoint(touch.clientX, touch.clientY);
const photoComp: any = elems.find((e) => e.classList.contains("p-outer")); const photoComp: any = elems.find((e) => e.classList.contains("p-outer"));
let overPhoto: IPhoto = photoComp?.__vue__?.data; let overPhoto: IPhoto = photoComp?.__vue__?.data;
if (overPhoto && overPhoto.flag & this.c.FLAG_PLACEHOLDER) overPhoto = null; if (overPhoto && overPhoto.flag & this.c.FLAG_PLACEHOLDER)
overPhoto = null;
// Do multi-selection "till" overPhoto "from" anchor // Do multi-selection "till" overPhoto "from" anchor
// This logic is completely different from the desktop because of the // This logic is completely different from the desktop because of the
@ -498,10 +504,10 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
this.$forceUpdate(); this.$forceUpdate();
} }
} },
/** Add a photo to selection list */ /** Add a photo to selection list */
public selectPhoto(photo: IPhoto, val?: boolean, noUpdate?: boolean) { selectPhoto(photo: IPhoto, val?: boolean, noUpdate?: boolean) {
if ( if (
photo.flag & this.c.FLAG_PLACEHOLDER || photo.flag & this.c.FLAG_PLACEHOLDER ||
photo.flag & this.c.FLAG_IS_FOLDER || photo.flag & this.c.FLAG_IS_FOLDER ||
@ -531,10 +537,10 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
this.updateHeadSelected(this.heads[photo.d.dayid]); this.updateHeadSelected(this.heads[photo.d.dayid]);
this.$forceUpdate(); this.$forceUpdate();
} }
} },
/** Multi-select */ /** Multi-select */
public 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);
if (pIdx === -1) return; if (pIdx === -1) return;
@ -575,7 +581,7 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
// Clear everything else in front // Clear everything else in front
Array.from(this.selection.values()) Array.from(this.selection.values())
.filter((p) => { .filter((p: IPhoto) => {
return this.isreverse return this.isreverse
? p.d.dayid > photo.d.dayid ? p.d.dayid > photo.d.dayid
: p.d.dayid < photo.d.dayid; : p.d.dayid < photo.d.dayid;
@ -589,10 +595,10 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
updateDaySet.forEach((d) => this.updateHeadSelected(this.heads[d])); updateDaySet.forEach((d) => this.updateHeadSelected(this.heads[d]));
this.$forceUpdate(); this.$forceUpdate();
} }
} },
/** Select or deselect all photos in a head */ /** Select or deselect all photos in a head */
public 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) {
@ -600,10 +606,10 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
} }
} }
this.$forceUpdate(); this.$forceUpdate();
} },
/** Check if the day for a photo is selected entirely */ /** Check if the day for a photo is selected entirely */
private updateHeadSelected(head: IHeadRow) { updateHeadSelected(head: IHeadRow) {
let selected = true; let selected = true;
// Check if all photos are selected // Check if all photos are selected
@ -618,10 +624,10 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
// Update head // Update head
head.selected = selected; head.selected = selected;
} },
/** Clear all selected photos */ /** Clear all selected photos */
public clearSelection(only?: IPhoto[]) { clearSelection(only?: IPhoto[]) {
const heads = new Set<IHeadRow>(); const heads = new Set<IHeadRow>();
const toClear = only || this.selection.values(); const toClear = only || this.selection.values();
Array.from(toClear).forEach((photo: IPhoto) => { Array.from(toClear).forEach((photo: IPhoto) => {
@ -632,10 +638,10 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
}); });
heads.forEach(this.updateHeadSelected); heads.forEach(this.updateHeadSelected);
this.$forceUpdate(); this.$forceUpdate();
} },
/** Restore selections from new day object */ /** Restore selections from new day object */
public restoreDay(day: IDay) { restoreDay(day: IDay) {
if (!this.has()) { if (!this.has()) {
return; return;
} }
@ -665,12 +671,12 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
}); });
this.selectionChanged(); this.selectionChanged();
} },
/** /**
* Download the currently selected files * Download the currently selected files
*/ */
private async downloadSelection(selection: Selection) { async downloadSelection(selection: Selection) {
if (selection.size >= 100) { if (selection.size >= 100) {
if ( if (
!confirm( !confirm(
@ -684,21 +690,21 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
} }
} }
await dav.downloadFilesByPhotos(Array.from(selection.values())); await dav.downloadFilesByPhotos(Array.from(selection.values()));
} },
/** /**
* Check if all files selected currently are favorites * Check if all files selected currently are favorites
*/ */
private allSelectedFavorites(selection: Selection) { allSelectedFavorites(selection: Selection) {
return Array.from(selection.values()).every( return Array.from(selection.values()).every(
(p) => p.flag & this.c.FLAG_IS_FAVORITE (p) => p.flag & this.c.FLAG_IS_FAVORITE
); );
} },
/** /**
* Favorite the currently selected photos * Favorite the currently selected photos
*/ */
private async favoriteSelection(selection: Selection) { async favoriteSelection(selection: Selection) {
const val = !this.allSelectedFavorites(selection); const val = !this.allSelectedFavorites(selection);
for await (const favIds of dav.favoritePhotos( for await (const favIds of dav.favoritePhotos(
Array.from(selection.values()), Array.from(selection.values()),
@ -706,12 +712,12 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
)) { )) {
} }
this.clearSelection(); this.clearSelection();
} },
/** /**
* Delete the currently selected photos * Delete the currently selected photos
*/ */
private async deleteSelection(selection: Selection) { async deleteSelection(selection: Selection) {
if (selection.size >= 100) { if (selection.size >= 100) {
if ( if (
!confirm( !confirm(
@ -733,36 +739,36 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
.map((id) => selection.get(id)); .map((id) => selection.get(id));
this.deletePhotos(delPhotos); this.deletePhotos(delPhotos);
} }
} },
/** /**
* Open the edit date dialog * Open the edit date dialog
*/ */
private async editDateSelection(selection: Selection) { async editDateSelection(selection: Selection) {
(<any>this.$refs.editDate).open(Array.from(selection.values())); (<any>this.$refs.editDate).open(Array.from(selection.values()));
} },
/** /**
* Open the edit date dialog * Open the edit date dialog
*/ */
private async editExifSelection(selection: Selection) { async editExifSelection(selection: Selection) {
if (selection.size !== 1) return; if (selection.size !== 1) return;
(<any>this.$refs.editExif).open(selection.values().next().value); (<any>this.$refs.editExif).open(selection.values().next().value);
} },
/** /**
* Open the files app with the selected file (one) * Open the files app with the selected file (one)
* Opens a new window. * Opens a new window.
*/ */
private async viewInFolder(selection: Selection) { async viewInFolder(selection: Selection) {
if (selection.size !== 1) return; if (selection.size !== 1) return;
dav.viewInFolder(selection.values().next().value); dav.viewInFolder(selection.values().next().value);
} },
/** /**
* Archive the currently selected photos * Archive the currently selected photos
*/ */
private async archiveSelection(selection: Selection) { async archiveSelection(selection: Selection) {
if (selection.size >= 100) { if (selection.size >= 100) {
if ( if (
!confirm( !confirm(
@ -787,19 +793,19 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
const delPhotos = delIds.map((id) => selection.get(id)); const delPhotos = delIds.map((id) => selection.get(id));
this.deletePhotos(delPhotos); this.deletePhotos(delPhotos);
} }
} },
/** /**
* Move selected photos to album * Move selected photos to album
*/ */
private async addToAlbum(selection: Selection) { async addToAlbum(selection: Selection) {
(<any>this.$refs.addToAlbumModal).open(Array.from(selection.values())); (<any>this.$refs.addToAlbumModal).open(Array.from(selection.values()));
} },
/** /**
* Move selected photos to another person * Move selected photos to another person
*/ */
private async moveSelectionToPerson(selection: Selection) { async moveSelectionToPerson(selection: Selection) {
if (!this.config_showFaceRect) { if (!this.config_showFaceRect) {
showError( showError(
this.t( this.t(
@ -810,12 +816,12 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
return; return;
} }
(<any>this.$refs.faceMoveModal).open(Array.from(selection.values())); (<any>this.$refs.faceMoveModal).open(Array.from(selection.values()));
} },
/** /**
* Remove currently selected photos from person * Remove currently selected photos from person
*/ */
private async removeSelectionFromPerson(selection: Selection) { async removeSelectionFromPerson(selection: Selection) {
// Make sure route is valid // Make sure route is valid
const { user, name } = this.$route.params; const { user, name } = this.$route.params;
if (this.$route.name !== "recognize" || !user || !name) { if (this.$route.name !== "recognize" || !user || !name) {
@ -838,20 +844,23 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
name, name,
Array.from(selection.values()) Array.from(selection.values())
)) { )) {
const delPhotos = delIds.filter((x) => x).map((id) => selection.get(id)); const delPhotos = delIds
.filter((x) => x)
.map((id) => selection.get(id));
this.deletePhotos(delPhotos); this.deletePhotos(delPhotos);
} }
} },
/** Open viewer with given photo */ /** Open viewer with given photo */
private openViewer(photo: IPhoto) { openViewer(photo: IPhoto) {
this.$router.push({ this.$router.push({
path: this.$route.path, path: this.$route.path,
query: this.$route.query, query: this.$route.query,
hash: utils.getViewerHash(photo), hash: utils.getViewerHash(photo),
}); });
} },
} },
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -69,9 +69,7 @@ input[type="text"] {
</style> </style>
<script lang="ts"> <script lang="ts">
import { Component, Mixins } from "vue-property-decorator"; import { defineComponent } from "vue";
import GlobalMixin from "../mixins/GlobalMixin";
import UserConfig from "../mixins/UserConfig";
import { getFilePickerBuilder } from "@nextcloud/dialogs"; import { getFilePickerBuilder } from "@nextcloud/dialogs";
const NcCheckboxRadioSwitch = () => const NcCheckboxRadioSwitch = () =>
@ -79,17 +77,21 @@ const NcCheckboxRadioSwitch = () =>
import MultiPathSelectionModal from "./modal/MultiPathSelectionModal.vue"; import MultiPathSelectionModal from "./modal/MultiPathSelectionModal.vue";
@Component({ export default defineComponent({
name: "Settings",
components: { components: {
NcCheckboxRadioSwitch, NcCheckboxRadioSwitch,
MultiPathSelectionModal, MultiPathSelectionModal,
}, },
})
export default class Settings extends Mixins(UserConfig, GlobalMixin) {
get pathSelTitle() {
return this.t("memories", "Choose Timeline Paths");
}
computed: {
pathSelTitle() {
return this.t("memories", "Choose Timeline Paths");
},
},
methods: {
async chooseFolder(title: string, initial: string) { async chooseFolder(title: string, initial: string) {
const picker = getFilePickerBuilder(title) const picker = getFilePickerBuilder(title)
.setMultiSelect(false) .setMultiSelect(false)
@ -101,11 +103,13 @@ export default class Settings extends Mixins(UserConfig, GlobalMixin) {
.build(); .build();
return await picker.pick(); return await picker.pick();
} },
async chooseTimelinePath() { async chooseTimelinePath() {
(<any>this.$refs.multiPathModal).open(this.config_timelinePath.split(";")); (<any>this.$refs.multiPathModal).open(
} this.config_timelinePath.split(";")
);
},
async saveTimelinePath(paths: string[]) { async saveTimelinePath(paths: string[]) {
if (!paths || !paths.length) return; if (!paths || !paths.length) return;
@ -115,7 +119,7 @@ export default class Settings extends Mixins(UserConfig, GlobalMixin) {
this.config_timelinePath = newPath; this.config_timelinePath = newPath;
await this.updateSetting("timelinePath"); await this.updateSetting("timelinePath");
} }
} },
async chooseFoldersPath() { async chooseFoldersPath() {
let newPath = await this.chooseFolder( let newPath = await this.chooseFolder(
@ -127,14 +131,15 @@ export default class Settings extends Mixins(UserConfig, GlobalMixin) {
this.config_foldersPath = newPath; this.config_foldersPath = newPath;
await this.updateSetting("foldersPath"); await this.updateSetting("foldersPath");
} }
} },
async updateSquareThumbs() { async updateSquareThumbs() {
await this.updateSetting("squareThumbs"); await this.updateSetting("squareThumbs");
} },
async updateShowHidden() { async updateShowHidden() {
await this.updateSetting("showHidden"); await this.updateSetting("showHidden");
} },
} },
});
</script> </script>

View File

@ -141,9 +141,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Mixins, Watch } from "vue-property-decorator"; import { defineComponent } from "vue";
import GlobalMixin from "../mixins/GlobalMixin";
import UserConfig from "../mixins/UserConfig";
import axios from "@nextcloud/axios"; import axios from "@nextcloud/axios";
import { showError } from "@nextcloud/dialogs"; import { showError } from "@nextcloud/dialogs";
@ -174,7 +172,9 @@ const SCROLL_LOAD_DELAY = 100; // Delay in loading data when scrolling
const DESKTOP_ROW_HEIGHT = 200; // Height of row on desktop const DESKTOP_ROW_HEIGHT = 200; // Height of row on desktop
const MOBILE_ROW_HEIGHT = 120; // Approx row height on mobile const MOBILE_ROW_HEIGHT = 120; // Approx row height on mobile
@Component({ export default defineComponent({
name: "Timeline",
components: { components: {
Folder, Folder,
Tag, Tag,
@ -191,55 +191,144 @@ const MOBILE_ROW_HEIGHT = 120; // Approx row height on mobile
PeopleIcon, PeopleIcon,
ImageMultipleIcon, ImageMultipleIcon,
}, },
})
export default class Timeline extends Mixins(GlobalMixin, UserConfig) { data() {
return {
/** Loading days response */ /** Loading days response */
private loading = 0; loading: 0,
/** Main list of rows */ /** Main list of rows */
private list: IRow[] = []; list: [] as IRow[],
/** Computed number of columns */ /** Computed number of columns */
private numCols = 0; numCols: 0,
/** Header rows for dayId key */ /** Header rows for dayId key */
private heads: { [dayid: number]: IHeadRow } = {}; heads: {} as { [dayid: number]: IHeadRow },
/** Computed row height */ /** Computed row height */
private rowHeight = 100; rowHeight: 100,
/** Computed row width */ /** Computed row width */
private rowWidth = 100; rowWidth: 100,
/** Current start index */ /** Current start index */
private currentStart = 0; currentStart: 0,
/** Current end index */ /** Current end index */
private currentEnd = 0; currentEnd: 0,
/** Resizing timer */ /** Resizing timer */
private resizeTimer = null as number | null; resizeTimer: null as number | null,
/** Height of the scroller */ /** Height of the scroller */
private scrollerHeight = 100; scrollerHeight: 100,
/** Set of dayIds for which images loaded */ /** Set of dayIds for which images loaded */
private loadedDays = new Set<number>(); loadedDays: new Set<number>(),
/** Set of dayIds for which image size is calculated */ /** Set of dayIds for which image size is calculated */
private sizedDays = new Set<number>(); sizedDays: new Set<number>(),
/** Days to load in the next call */ /** Days to load in the next call */
private fetchDayQueue = [] as number[]; fetchDayQueue: [] as number[],
/** Timer to load day call */ /** Timer to load day call */
private fetchDayTimer = null as number | null; fetchDayTimer: null as number | null,
/** State for request cancellations */ /** State for request cancellations */
private state = Math.random(); state: Math.random(),
/** Selection manager component */ /** Selection manager component */
private selectionManager!: SelectionManager & any; selectionManager: null as SelectionManager & any,
/** Scroller manager component */ /** Scroller manager component */
private scrollerManager!: ScrollerManager & any; scrollerManager: null as ScrollerManager & any,
};
},
mounted() { mounted() {
this.selectionManager = this.$refs.selectionManager; this.selectionManager = this.$refs.selectionManager;
this.scrollerManager = this.$refs.scrollerManager; this.scrollerManager = this.$refs.scrollerManager;
this.routeChange(this.$route); this.routeChange(this.$route);
} },
@Watch("$route") watch: {
async $route(to: any, from?: any) {
await this.routeChange(to, from);
},
},
beforeDestroy() {
unsubscribe(this.config_eventName, this.softRefresh);
unsubscribe("files:file:created", this.softRefresh);
this.resetState();
},
created() {
subscribe(this.config_eventName, this.softRefresh);
subscribe("files:file:created", this.softRefresh);
window.addEventListener("resize", this.handleResizeWithDelay);
},
destroyed() {
window.removeEventListener("resize", this.handleResizeWithDelay);
},
computed: {
routeIsBase() {
return this.$route.name === "timeline";
},
routeIsPeople() {
return ["recognize", "facerecognition"].includes(this.$route.name);
},
routeIsArchive() {
return this.$route.name === "archive";
},
isMonthView() {
return this.$route.name === "albums";
},
/** Get view name for dynamic top matter */
viewName() {
switch (this.$route.name) {
case "timeline":
return this.t("memories", "Your Timeline");
case "favorites":
return this.t("memories", "Favorites");
case "recognize":
case "facerecognition":
return this.t("memories", "People");
case "videos":
return this.t("memories", "Videos");
case "albums":
return this.t("memories", "Albums");
case "archive":
return this.t("memories", "Archive");
case "thisday":
return this.t("memories", "On this day");
case "tags":
return this.t("memories", "Tags");
default:
return "";
}
},
emptyViewDescription() {
switch (this.$route.name) {
case "facerecognition":
if (this.config_facerecognitionEnabled)
return this.t(
"memories",
"You will find your friends soon. Please, be patient."
);
else
return this.t(
"memories",
"Face Recognition is disabled. Enable in settings to find your friends."
);
case "timeline":
case "favorites":
case "recognize":
case "videos":
case "albums":
case "archive":
case "thisday":
case "tags":
default:
return "";
}
},
},
methods: {
async routeChange(to: any, from?: any) { async routeChange(to: any, from?: any) {
if (from?.path !== to.path) { if (from?.path !== to.path) {
await this.refresh(); await this.refresh();
@ -250,7 +339,11 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
// Check if hash has changed // Check if hash has changed
const viewerIsOpen = (this.$refs.viewer as any).isOpen; const viewerIsOpen = (this.$refs.viewer as any).isOpen;
if (from?.hash !== to.hash && to.hash?.startsWith("#v") && !viewerIsOpen) { if (
from?.hash !== to.hash &&
to.hash?.startsWith("#v") &&
!viewerIsOpen
) {
// Open viewer // Open viewer
const parts = to.hash.split("/"); const parts = to.hash.split("/");
if (parts.length !== 3) return; if (parts.length !== 3) return;
@ -291,55 +384,22 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
// Close viewer // Close viewer
(this.$refs.viewer as any).close(); (this.$refs.viewer as any).close();
} }
} },
beforeDestroy() {
unsubscribe(this.config_eventName, this.softRefresh);
unsubscribe("files:file:created", this.softRefresh);
this.resetState();
}
created() {
subscribe(this.config_eventName, this.softRefresh);
subscribe("files:file:created", this.softRefresh);
window.addEventListener("resize", this.handleResizeWithDelay);
}
destroyed() {
window.removeEventListener("resize", this.handleResizeWithDelay);
}
get routeIsBase() {
return this.$route.name === "timeline";
}
get routeIsPeople() {
return ["recognize", "facerecognition"].includes(this.$route.name);
}
get routeIsArchive() {
return this.$route.name === "archive";
}
updateLoading(delta: number) { updateLoading(delta: number) {
this.loading += delta; this.loading += delta;
} },
isMobile() { isMobile() {
return globalThis.windowInnerWidth <= 768; return globalThis.windowInnerWidth <= 768;
} },
isMobileLayout() { isMobileLayout() {
return globalThis.windowInnerWidth <= 600; return globalThis.windowInnerWidth <= 600;
} },
get isMonthView() {
return this.$route.name === "albums";
}
allowBreakout() { allowBreakout() {
return this.isMobileLayout() && !this.config_squareThumbs; return this.isMobileLayout() && !this.config_squareThumbs;
} },
/** Create new state */ /** Create new state */
async createState() { async createState() {
@ -358,7 +418,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
// Get data // Get data
await this.fetchDays(); await this.fetchDays();
} },
/** Reset all state */ /** Reset all state */
async resetState() { async resetState() {
@ -375,19 +435,19 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
this.fetchDayQueue = []; this.fetchDayQueue = [];
window.clearTimeout(this.fetchDayTimer); window.clearTimeout(this.fetchDayTimer);
window.clearTimeout(this.resizeTimer); window.clearTimeout(this.resizeTimer);
} },
/** Recreate everything */ /** Recreate everything */
async refresh() { async refresh() {
await this.resetState(); await this.resetState();
await this.createState(); await this.createState();
} },
/** Re-process days */ /** Re-process days */
async softRefresh() { async softRefresh() {
this.selectionManager.clearSelection(); this.selectionManager.clearSelection();
await this.fetchDays(true); await this.fetchDays(true);
} },
/** Do resize after some time */ /** Do resize after some time */
handleResizeWithDelay() { handleResizeWithDelay() {
@ -403,7 +463,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
this.recomputeSizes(); this.recomputeSizes();
this.resizeTimer = null; this.resizeTimer = null;
}, 100); }, 100);
} },
/** Recompute static sizes of containers */ /** Recompute static sizes of containers */
recomputeSizes() { recomputeSizes() {
@ -442,7 +502,10 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
if (this.isMobileLayout()) { if (this.isMobileLayout()) {
// Mobile // Mobile
this.numCols = Math.max(3, Math.floor(this.rowWidth / MOBILE_ROW_HEIGHT)); this.numCols = Math.max(
3,
Math.floor(this.rowWidth / MOBILE_ROW_HEIGHT)
);
this.rowHeight = Math.floor(this.rowWidth / this.numCols); this.rowHeight = Math.floor(this.rowWidth / this.numCols);
} else { } else {
// Desktop // Desktop
@ -470,7 +533,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
// Explicitly request a scroll event // Explicitly request a scroll event
this.loadScrollChanges(this.currentStart, this.currentEnd); this.loadScrollChanges(this.currentStart, this.currentEnd);
} }
} },
/** /**
* Triggered when position of scroll change. * Triggered when position of scroll change.
@ -479,7 +542,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
*/ */
scrollPositionChange(event?: any) { scrollPositionChange(event?: any) {
this.scrollerManager.recyclerScrolled(event); this.scrollerManager.recyclerScrolled(event);
} },
/** Trigger when recycler view changes */ /** Trigger when recycler view changes */
scrollChange(startIndex: number, endIndex: number) { scrollChange(startIndex: number, endIndex: number) {
@ -537,7 +600,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
this.loadScrollChanges(start, end); this.loadScrollChanges(start, end);
} }
}, SCROLL_LOAD_DELAY); }, SCROLL_LOAD_DELAY);
} },
/** Load image data for given view */ /** Load image data for given view */
loadScrollChanges(startIndex: number, endIndex: number) { loadScrollChanges(startIndex: number, endIndex: number) {
@ -559,7 +622,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
this.fetchDay(item.dayId); this.fetchDay(item.dayId);
} }
} },
/** Get query string for API calls */ /** Get query string for API calls */
getQuery() { getQuery() {
@ -622,32 +685,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
} }
return query; return query;
} },
/** Get view name for dynamic top matter */
get viewName() {
switch (this.$route.name) {
case "timeline":
return this.t("memories", "Your Timeline");
case "favorites":
return this.t("memories", "Favorites");
case "recognize":
case "facerecognition":
return this.t("memories", "People");
case "videos":
return this.t("memories", "Videos");
case "albums":
return this.t("memories", "Albums");
case "archive":
return this.t("memories", "Archive");
case "thisday":
return this.t("memories", "On this day");
case "tags":
return this.t("memories", "Tags");
default:
return "";
}
}
/** Get name of header */ /** Get name of header */
getHeadName(head: IHeadRow) { getHeadName(head: IHeadRow) {
@ -675,34 +713,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
// Cache and return // Cache and return
head.name = name; head.name = name;
return head.name; return head.name;
} },
/* Get a friendly description of empty view */
get emptyViewDescription() {
switch (this.$route.name) {
case "facerecognition":
if (this.config_facerecognitionEnabled)
return this.t(
"memories",
"You will find your friends soon. Please, be patient."
);
else
return this.t(
"memories",
"Face Recognition is disabled. Enable in settings to find your friends."
);
case "timeline":
case "favorites":
case "recognize":
case "videos":
case "albums":
case "archive":
case "thisday":
case "tags":
default:
return "";
}
}
/** Fetch timeline main call */ /** Fetch timeline main call */
async fetchDays(noCache = false) { async fetchDays(noCache = false) {
@ -759,7 +770,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
} finally { } finally {
if (!cache) this.loading--; if (!cache) this.loading--;
} }
} },
/** Process the data for days call including folders */ /** Process the data for days call including folders */
async processDays(data: IDay[]) { async processDays(data: IDay[]) {
@ -868,12 +879,12 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
// Fix view height variable // Fix view height variable
await this.scrollerManager.reflow(); await this.scrollerManager.reflow();
this.scrollPositionChange(); this.scrollPositionChange();
} },
/** API url for Day call */ /** API url for Day call */
private getDayUrl(dayId: number | string) { getDayUrl(dayId: number | string) {
return API.Q(API.DAY(dayId), this.getQuery()); return API.Q(API.DAY(dayId), this.getQuery());
} },
/** Fetch image data for one dayId */ /** Fetch image data for one dayId */
async fetchDay(dayId: number, now = false) { async fetchDay(dayId: number, now = false) {
@ -907,7 +918,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
this.fetchDayExpire(); this.fetchDayExpire();
}, 150); }, 150);
} }
} },
async fetchDayExpire() { async fetchDayExpire() {
if (this.fetchDayQueue.length === 0) return; if (this.fetchDayQueue.length === 0) return;
@ -966,7 +977,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
showError(this.t("memories", "Failed to load some photos")); showError(this.t("memories", "Failed to load some photos"));
console.error(e); console.error(e);
} }
} },
/** /**
* Process items from day response. * Process items from day response.
@ -992,7 +1003,8 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
(p) => (p) =>
!( !(
p.flag & this.c.FLAG_IS_FOLDER && p.flag & this.c.FLAG_IS_FOLDER &&
((<IFolder>p).name.startsWith(".") || !(<IFolder>p).previews.length) ((<IFolder>p).name.startsWith(".") ||
!(<IFolder>p).previews.length)
) )
); );
} }
@ -1209,7 +1221,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
// Scroll to new position // Scroll to new position
(<any>this.$refs.recycler).$el.scrollTop = scrollTop; (<any>this.$refs.recycler).$el.scrollTop = scrollTop;
} }
} },
/** Add and get a new blank photos row */ /** Add and get a new blank photos row */
addRow(day: IDay): IRow { addRow(day: IDay): IRow {
@ -1233,7 +1245,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
day.rows.push(row); day.rows.push(row);
return row; return row;
} },
/** /**
* Delete elements from main view with some animation * Delete elements from main view with some animation
@ -1269,8 +1281,9 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
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);
} }
} },
} },
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -29,41 +29,66 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Prop, Watch, Mixins } from "vue-property-decorator"; import { defineComponent, PropType } from "vue";
import { IFolder, IPhoto } from "../../types"; import { IFolder, IPhoto } from "../../types";
import GlobalMixin from "../../mixins/GlobalMixin";
import UserConfig from "../../mixins/UserConfig";
import { getPreviewUrl } from "../../services/FileUtils"; import { getPreviewUrl } from "../../services/FileUtils";
import FolderIcon from "vue-material-design-icons/Folder.vue"; import FolderIcon from "vue-material-design-icons/Folder.vue";
@Component({ export default defineComponent({
name: "Folder",
components: { components: {
FolderIcon, FolderIcon,
}, },
})
export default class Folder extends Mixins(GlobalMixin, UserConfig) {
@Prop() data: IFolder;
props: {
data: Object as PropType<IFolder>,
},
data() {
return {
// Separate property because the one on data isn't reactive // Separate property because the one on data isn't reactive
private previews: IPhoto[] = []; previews: [] as IPhoto[],
// Error occured fetching thumbs // Error occured fetching thumbs
private error = false; error: false,
// Passthrough
getPreviewUrl,
};
},
/** Passthrough */ computed: {
private getPreviewUrl = getPreviewUrl; /** Open folder */
target() {
const path = this.data.path
.split("/")
.filter((x) => x)
.slice(2) as string[];
// Remove base path if present
const basePath = this.config_foldersPath.split("/").filter((x) => x);
if (
path.length >= basePath.length &&
path.slice(0, basePath.length).every((x, i) => x === basePath[i])
) {
path.splice(0, basePath.length);
}
return { name: "folders", params: { path: path as any } };
},
},
mounted() { mounted() {
this.refreshPreviews(); this.refreshPreviews();
} },
@Watch("data") watch: {
dataChanged() { data() {
this.refreshPreviews(); this.refreshPreviews();
} },
},
methods: {
/** Refresh previews */ /** Refresh previews */
refreshPreviews() { refreshPreviews() {
// Reset state // Reset state
@ -84,27 +109,9 @@ export default class Folder extends Mixins(GlobalMixin, UserConfig) {
this.previews = previews.slice(0, 4); this.previews = previews.slice(0, 4);
} }
} }
} },
},
/** Open folder */ });
get target() {
const path = this.data.path
.split("/")
.filter((x) => x)
.slice(2) as string[];
// Remove base path if present
const basePath = this.config_foldersPath.split("/").filter((x) => x);
if (
path.length >= basePath.length &&
path.slice(0, basePath.length).every((x, i) => x === basePath[i])
) {
path.splice(0, basePath.length);
}
return { name: "folders", params: { path: path as any } };
}
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -4,6 +4,8 @@ import "reflect-metadata";
import Vue from "vue"; import Vue from "vue";
import VueVirtualScroller from "vue-virtual-scroller"; import VueVirtualScroller from "vue-virtual-scroller";
import "vue-virtual-scroller/dist/vue-virtual-scroller.css"; import "vue-virtual-scroller/dist/vue-virtual-scroller.css";
import GlobalMixin from "./mixins/GlobalMixin";
import UserConfig from "./mixins/UserConfig";
import App from "./App.vue"; import App from "./App.vue";
import router from "./router"; import router from "./router";
@ -64,6 +66,8 @@ if (!globalThis.videoClientIdPersistent) {
); );
} }
Vue.mixin(GlobalMixin);
Vue.mixin(UserConfig);
Vue.use(VueVirtualScroller); Vue.use(VueVirtualScroller);
// https://github.com/nextcloud/photos/blob/156f280c0476c483cb9ce81769ccb0c1c6500a4e/src/main.js // https://github.com/nextcloud/photos/blob/156f280c0476c483cb9ce81769ccb0c1c6500a4e/src/main.js