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>
<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 NcAppContent from "@nextcloud/vue/dist/Components/NcAppContent";
@ -49,15 +49,12 @@ const NcAppNavigationSettings = () =>
import("@nextcloud/vue/dist/Components/NcAppNavigationSettings");
import { generateUrl } from "@nextcloud/router";
import { getCurrentUser } from "@nextcloud/auth";
import { translate as t } from "@nextcloud/l10n";
import Timeline from "./components/Timeline.vue";
import Settings from "./components/Settings.vue";
import FirstStart from "./components/FirstStart.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 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 MapIcon from "vue-material-design-icons/Map.vue";
@Component({
export default defineComponent({
name: "App",
components: {
NcContent,
NcAppContent,
@ -93,13 +91,109 @@ import MapIcon from "vue-material-design-icons/Map.vue";
TagsIcon,
MapIcon,
},
})
export default class App extends Mixins(GlobalMixin, UserConfig) {
// Outer element
private metadataComponent!: Metadata;
data() {
return {
navItems: [],
metadataComponent: null as Metadata,
};
},
private readonly navItemsAll = (self: typeof this) => [
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;
},
})
);
}
},
methods: {
navItemsAll() {
return [
{
name: "timeline",
icon: ImageMultiple,
@ -124,19 +218,19 @@ export default class App extends Mixins(GlobalMixin, UserConfig) {
name: "albums",
icon: AlbumIcon,
title: t("memories", "Albums"),
if: self.showAlbums,
if: this.showAlbums,
},
{
name: "recognize",
icon: PeopleIcon,
title: self.recognize,
if: self.recognize,
title: this.recognize,
if: this.recognize,
},
{
name: "facerecognition",
icon: PeopleIcon,
title: self.facerecognition,
if: self.facerecognition,
title: this.facerecognition,
if: this.facerecognition,
},
{
name: "archive",
@ -152,115 +246,16 @@ export default class App extends Mixins(GlobalMixin, UserConfig) {
name: "tags",
icon: TagsIcon,
title: t("memories", "Tags"),
if: self.config_tagsEnabled,
if: this.config_tagsEnabled,
},
{
name: "maps",
icon: MapIcon,
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() {
if ("serviceWorker" in navigator) {
@ -279,18 +274,18 @@ export default class App extends Mixins(GlobalMixin, UserConfig) {
} else {
console.debug("Service Worker is not enabled on this browser.");
}
}
},
linkClick() {
const nav: any = this.$refs.nav;
if (globalThis.windowInnerWidth <= 1024) nav?.toggleNavigation(false);
}
},
doRouteChecks() {
if (this.$route.name === "folder-share") {
this.putFolderShareToken(this.$route.params.token);
}
}
},
putFolderShareToken(token: string) {
// 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;
}
}
},
},
});
</script>
<style scoped lang="scss">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -141,9 +141,7 @@
</template>
<script lang="ts">
import { Component, Mixins, Watch } from "vue-property-decorator";
import GlobalMixin from "../mixins/GlobalMixin";
import UserConfig from "../mixins/UserConfig";
import { defineComponent } from "vue";
import axios from "@nextcloud/axios";
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 MOBILE_ROW_HEIGHT = 120; // Approx row height on mobile
@Component({
export default defineComponent({
name: "Timeline",
components: {
Folder,
Tag,
@ -191,55 +191,144 @@ const MOBILE_ROW_HEIGHT = 120; // Approx row height on mobile
PeopleIcon,
ImageMultipleIcon,
},
})
export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
data() {
return {
/** Loading days response */
private loading = 0;
loading: 0,
/** Main list of rows */
private list: IRow[] = [];
list: [] as IRow[],
/** Computed number of columns */
private numCols = 0;
numCols: 0,
/** Header rows for dayId key */
private heads: { [dayid: number]: IHeadRow } = {};
heads: {} as { [dayid: number]: IHeadRow },
/** Computed row height */
private rowHeight = 100;
rowHeight: 100,
/** Computed row width */
private rowWidth = 100;
rowWidth: 100,
/** Current start index */
private currentStart = 0;
currentStart: 0,
/** Current end index */
private currentEnd = 0;
currentEnd: 0,
/** Resizing timer */
private resizeTimer = null as number | null;
resizeTimer: null as number | null,
/** Height of the scroller */
private scrollerHeight = 100;
scrollerHeight: 100,
/** 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 */
private sizedDays = new Set<number>();
sizedDays: new Set<number>(),
/** Days to load in the next call */
private fetchDayQueue = [] as number[];
fetchDayQueue: [] as number[],
/** Timer to load day call */
private fetchDayTimer = null as number | null;
fetchDayTimer: null as number | null,
/** State for request cancellations */
private state = Math.random();
state: Math.random(),
/** Selection manager component */
private selectionManager!: SelectionManager & any;
selectionManager: null as SelectionManager & any,
/** Scroller manager component */
private scrollerManager!: ScrollerManager & any;
scrollerManager: null as ScrollerManager & any,
};
},
mounted() {
this.selectionManager = this.$refs.selectionManager;
this.scrollerManager = this.$refs.scrollerManager;
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) {
if (from?.path !== to.path) {
await this.refresh();
@ -250,7 +339,11 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
// Check if hash has changed
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
const parts = to.hash.split("/");
if (parts.length !== 3) return;
@ -291,55 +384,22 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
// Close viewer
(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) {
this.loading += delta;
}
},
isMobile() {
return globalThis.windowInnerWidth <= 768;
}
},
isMobileLayout() {
return globalThis.windowInnerWidth <= 600;
}
get isMonthView() {
return this.$route.name === "albums";
}
},
allowBreakout() {
return this.isMobileLayout() && !this.config_squareThumbs;
}
},
/** Create new state */
async createState() {
@ -358,7 +418,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
// Get data
await this.fetchDays();
}
},
/** Reset all state */
async resetState() {
@ -375,19 +435,19 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
this.fetchDayQueue = [];
window.clearTimeout(this.fetchDayTimer);
window.clearTimeout(this.resizeTimer);
}
},
/** Recreate everything */
async refresh() {
await this.resetState();
await this.createState();
}
},
/** Re-process days */
async softRefresh() {
this.selectionManager.clearSelection();
await this.fetchDays(true);
}
},
/** Do resize after some time */
handleResizeWithDelay() {
@ -403,7 +463,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
this.recomputeSizes();
this.resizeTimer = null;
}, 100);
}
},
/** Recompute static sizes of containers */
recomputeSizes() {
@ -442,7 +502,10 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
if (this.isMobileLayout()) {
// 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);
} else {
// Desktop
@ -470,7 +533,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
// Explicitly request a scroll event
this.loadScrollChanges(this.currentStart, this.currentEnd);
}
}
},
/**
* Triggered when position of scroll change.
@ -479,7 +542,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
*/
scrollPositionChange(event?: any) {
this.scrollerManager.recyclerScrolled(event);
}
},
/** Trigger when recycler view changes */
scrollChange(startIndex: number, endIndex: number) {
@ -537,7 +600,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
this.loadScrollChanges(start, end);
}
}, SCROLL_LOAD_DELAY);
}
},
/** Load image data for given view */
loadScrollChanges(startIndex: number, endIndex: number) {
@ -559,7 +622,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
this.fetchDay(item.dayId);
}
}
},
/** Get query string for API calls */
getQuery() {
@ -622,32 +685,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
}
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 */
getHeadName(head: IHeadRow) {
@ -675,34 +713,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
// Cache and return
head.name = 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 */
async fetchDays(noCache = false) {
@ -759,7 +770,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
} finally {
if (!cache) this.loading--;
}
}
},
/** Process the data for days call including folders */
async processDays(data: IDay[]) {
@ -868,12 +879,12 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
// Fix view height variable
await this.scrollerManager.reflow();
this.scrollPositionChange();
}
},
/** API url for Day call */
private getDayUrl(dayId: number | string) {
getDayUrl(dayId: number | string) {
return API.Q(API.DAY(dayId), this.getQuery());
}
},
/** Fetch image data for one dayId */
async fetchDay(dayId: number, now = false) {
@ -907,7 +918,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
this.fetchDayExpire();
}, 150);
}
}
},
async fetchDayExpire() {
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"));
console.error(e);
}
}
},
/**
* Process items from day response.
@ -992,7 +1003,8 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
(p) =>
!(
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
(<any>this.$refs.recycler).$el.scrollTop = scrollTop;
}
}
},
/** Add and get a new blank photos row */
addRow(day: IDay): IRow {
@ -1233,7 +1245,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
day.rows.push(row);
return row;
}
},
/**
* 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));
this.processDay(day.dayid, newDetail);
}
}
}
},
},
});
</script>
<style lang="scss" scoped>

View File

@ -29,41 +29,66 @@
</template>
<script lang="ts">
import { Component, Prop, Watch, Mixins } from "vue-property-decorator";
import { defineComponent, PropType } from "vue";
import { IFolder, IPhoto } from "../../types";
import GlobalMixin from "../../mixins/GlobalMixin";
import UserConfig from "../../mixins/UserConfig";
import { getPreviewUrl } from "../../services/FileUtils";
import FolderIcon from "vue-material-design-icons/Folder.vue";
@Component({
export default defineComponent({
name: "Folder",
components: {
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
private previews: IPhoto[] = [];
previews: [] as IPhoto[],
// Error occured fetching thumbs
private error = false;
error: false,
// Passthrough
getPreviewUrl,
};
},
/** Passthrough */
private getPreviewUrl = getPreviewUrl;
computed: {
/** 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() {
this.refreshPreviews();
}
},
@Watch("data")
dataChanged() {
watch: {
data() {
this.refreshPreviews();
}
},
},
methods: {
/** Refresh previews */
refreshPreviews() {
// Reset state
@ -84,27 +109,9 @@ export default class Folder extends Mixins(GlobalMixin, UserConfig) {
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>
<style lang="scss" scoped>

View File

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