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,132 +91,66 @@ 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) => [
{
name: "timeline",
icon: ImageMultiple,
title: t("memories", "Timeline"),
computed: {
ncVersion() {
const version = (<any>window.OC).config.version.split(".");
return Number(version[0]);
},
{
name: "folders",
icon: FolderIcon,
title: t("memories", "Folders"),
recognize() {
if (!this.config_recognizeEnabled) {
return false;
}
if (this.config_facerecognitionInstalled) {
return t("memories", "People (Recognize)");
}
return t("memories", "People");
},
{
name: "favorites",
icon: Star,
title: t("memories", "Favorites"),
facerecognition() {
if (!this.config_facerecognitionInstalled) {
return false;
}
if (this.config_recognizeEnabled) {
return t("memories", "People (Face Recognition)");
}
return t("memories", "People");
},
{
name: "videos",
icon: Video,
title: t("memories", "Videos"),
isFirstStart() {
return this.config_timelinePath === "EMPTY";
},
{
name: "albums",
icon: AlbumIcon,
title: t("memories", "Albums"),
if: self.showAlbums,
showAlbums() {
return this.config_albumsEnabled;
},
{
name: "recognize",
icon: PeopleIcon,
title: self.recognize,
if: self.recognize,
removeOuterGap() {
return this.ncVersion >= 25;
},
{
name: "facerecognition",
icon: PeopleIcon,
title: self.facerecognition,
if: self.facerecognition,
showNavigation() {
return this.$route.name !== "folder-share";
},
{
name: "archive",
icon: ArchiveIcon,
title: t("memories", "Archive"),
},
watch: {
route() {
this.doRouteChecks();
},
{
name: "thisday",
icon: CalendarIcon,
title: t("memories", "On this day"),
},
{
name: "tags",
icon: TagsIcon,
title: t("memories", "Tags"),
if: self.config_tagsEnabled,
},
{
name: "maps",
icon: MapIcon,
title: t("memories", "Maps"),
if: self.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(
this.navItems = this.navItemsAll().filter(
(item) => typeof item.if === "undefined" || Boolean(item.if)
);
@ -242,10 +174,7 @@ export default class App extends Mixins(GlobalMixin, UserConfig) {
if (this.metadataComponent) {
this.metadataComponent.$destroy();
}
this.metadataComponent = new Metadata({
// Better integration with vue parent component
parent: context,
});
this.metadataComponent = new Vue(Metadata);
// Only mount after we have all the info we need
await this.metadataComponent.update(fileInfo);
this.metadataComponent.$mount(el);
@ -260,56 +189,123 @@ export default class App extends Mixins(GlobalMixin, UserConfig) {
})
);
}
}
},
async beforeMount() {
if ("serviceWorker" in navigator) {
// Use the window load event to keep the page load performant
window.addEventListener("load", async () => {
try {
const url = generateUrl("/apps/memories/service-worker.js");
const registration = await navigator.serviceWorker.register(url, {
scope: generateUrl("/apps/memories"),
});
console.log("SW registered: ", registration);
} catch (error) {
console.error("SW registration failed: ", error);
}
});
} else {
console.debug("Service Worker is not enabled on this browser.");
}
}
methods: {
navItemsAll() {
return [
{
name: "timeline",
icon: ImageMultiple,
title: t("memories", "Timeline"),
},
{
name: "folders",
icon: FolderIcon,
title: t("memories", "Folders"),
},
{
name: "favorites",
icon: Star,
title: t("memories", "Favorites"),
},
{
name: "videos",
icon: Video,
title: t("memories", "Videos"),
},
{
name: "albums",
icon: AlbumIcon,
title: t("memories", "Albums"),
if: this.showAlbums,
},
{
name: "recognize",
icon: PeopleIcon,
title: this.recognize,
if: this.recognize,
},
{
name: "facerecognition",
icon: PeopleIcon,
title: this.facerecognition,
if: this.facerecognition,
},
{
name: "archive",
icon: ArchiveIcon,
title: t("memories", "Archive"),
},
{
name: "thisday",
icon: CalendarIcon,
title: t("memories", "On this day"),
},
{
name: "tags",
icon: TagsIcon,
title: t("memories", "Tags"),
if: this.config_tagsEnabled,
},
{
name: "maps",
icon: MapIcon,
title: t("memories", "Maps"),
if: this.config_mapsEnabled,
},
];
},
linkClick() {
const nav: any = this.$refs.nav;
if (globalThis.windowInnerWidth <= 1024) nav?.toggleNavigation(false);
}
async beforeMount() {
if ("serviceWorker" in navigator) {
// Use the window load event to keep the page load performant
window.addEventListener("load", async () => {
try {
const url = generateUrl("/apps/memories/service-worker.js");
const registration = await navigator.serviceWorker.register(url, {
scope: generateUrl("/apps/memories"),
});
console.log("SW registered: ", registration);
} catch (error) {
console.error("SW registration failed: ", error);
}
});
} else {
console.debug("Service Worker is not enabled on this browser.");
}
},
doRouteChecks() {
if (this.$route.name === "folder-share") {
this.putFolderShareToken(this.$route.params.token);
}
}
linkClick() {
const nav: any = this.$refs.nav;
if (globalThis.windowInnerWidth <= 1024) nav?.toggleNavigation(false);
},
putFolderShareToken(token: string) {
// Viewer looks for an input with ID sharingToken with the value as the token
// Create this element or update it otherwise files not gonna open
// https://github.com/nextcloud/viewer/blob/a8c46050fb687dcbb48a022a15a5d1275bf54a8e/src/utils/davUtils.js#L61
let tokenInput = document.getElementById(
"sharingToken"
) as HTMLInputElement;
if (!tokenInput) {
tokenInput = document.createElement("input");
tokenInput.id = "sharingToken";
tokenInput.type = "hidden";
tokenInput.style.display = "none";
document.body.appendChild(tokenInput);
}
doRouteChecks() {
if (this.$route.name === "folder-share") {
this.putFolderShareToken(this.$route.params.token);
}
},
tokenInput.value = token;
}
}
putFolderShareToken(token: string) {
// Viewer looks for an input with ID sharingToken with the value as the token
// Create this element or update it otherwise files not gonna open
// https://github.com/nextcloud/viewer/blob/a8c46050fb687dcbb48a022a15a5d1275bf54a8e/src/utils/davUtils.js#L61
let tokenInput = document.getElementById(
"sharingToken"
) as HTMLInputElement;
if (!tokenInput) {
tokenInput = document.createElement("input");
tokenInput.id = "sharingToken";
tokenInput.type = "hidden";
tokenInput.style.display = "none";
document.body.appendChild(tokenInput);
}
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,95 +57,100 @@ 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() {
return getCurrentUser().isAdmin;
}
computed: {
isAdmin() {
return getCurrentUser().isAdmin;
},
},
async begin() {
const path = await this.chooseFolder(
this.t("memories", "Choose the root of your timeline"),
"/"
);
// Get folder days
this.error = "";
this.info = "";
const query = new URLSearchParams();
query.set("timelinePath", path);
let url = API.Q(API.DAYS(), query);
const res = await axios.get<IDay[]>(url);
// Check response
if (res.status !== 200) {
this.error = this.t(
"memories",
"The selected folder does not seem to be valid. Try again."
methods: {
async begin() {
const path = await this.chooseFolder(
this.t("memories", "Choose the root of your timeline"),
"/"
);
return;
}
// Count total photos
const n = res.data.reduce((acc, day) => acc + day.count, 0);
this.info = this.n(
"memories",
"Found {n} item in {path}",
"Found {n} items in {path}",
n,
{
n,
path,
// Get folder days
this.error = "";
this.info = "";
const query = new URLSearchParams();
query.set("timelinePath", path);
let url = API.Q(API.DAYS(), query);
const res = await axios.get<IDay[]>(url);
// Check response
if (res.status !== 200) {
this.error = this.t(
"memories",
"The selected folder does not seem to be valid. Try again."
);
return;
}
);
this.chosenPath = path;
}
async finish() {
this.show = false;
await new Promise((resolve) => setTimeout(resolve, 500));
this.config_timelinePath = this.chosenPath;
await this.updateSetting("timelinePath");
}
// Count total photos
const n = res.data.reduce((acc, day) => acc + day.count, 0);
this.info = this.n(
"memories",
"Found {n} item in {path}",
"Found {n} items in {path}",
n,
{
n,
path,
}
);
this.chosenPath = path;
},
async chooseFolder(title: string, initial: string) {
const picker = getFilePickerBuilder(title)
.setMultiSelect(false)
.setModal(true)
.setType(1)
.addMimeTypeFilter("httpd/unix-directory")
.allowDirectories()
.startAt(initial)
.build();
async finish() {
this.show = false;
await new Promise((resolve) => setTimeout(resolve, 500));
this.config_timelinePath = this.chosenPath;
await this.updateSetting("timelinePath");
},
return await picker.pick();
}
}
async chooseFolder(title: string, initial: string) {
const picker = getFilePickerBuilder(title)
.setMultiSelect(false)
.setModal(true)
.setType(1)
.addMimeTypeFilter("httpd/unix-directory")
.allowDirectories()
.startAt(initial)
.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,258 +67,266 @@ 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);
}
}
computed: {
topFields() {
let list: {
title: string;
subtitle: string[];
icon: any;
href?: string;
edit?: () => void;
}[] = [];
get topFields() {
let list: {
title: string;
subtitle: string[];
icon: any;
href?: string;
edit?: () => void;
}[] = [];
if (this.dateOriginal) {
list.push({
title: this.dateOriginalStr,
subtitle: this.dateOriginalTime,
icon: CalendarIcon,
edit: () => globalThis.editDate(globalThis.currentViewerPhoto),
});
}
if (this.camera) {
list.push({
title: this.camera,
subtitle: this.cameraSub,
icon: CameraIrisIcon,
});
}
if (this.imageInfo) {
list.push({
title: this.imageInfo,
subtitle: this.imageInfoSub,
icon: ImageIcon,
});
}
const title = this.exif?.["Title"];
const desc = this.exif?.["Description"];
if (title || desc) {
list.push({
title: title || this.t("memories", "No title"),
subtitle: [desc || this.t("memories", "No description")],
icon: InfoIcon,
edit: () => globalThis.editExif(globalThis.currentViewerPhoto),
});
}
if (this.address) {
list.push({
title: this.address,
subtitle: [],
icon: LocationIcon,
href: this.mapFullUrl,
});
}
return list;
}
/** Date taken info */
get dateOriginal() {
const dt = this.exif["DateTimeOriginal"] || this.exif["CreateDate"];
if (!dt) return null;
const m = moment.utc(dt, "YYYY:MM:DD HH:mm:ss");
if (!m.isValid()) return null;
m.locale(getCanonicalLocale());
return m;
}
get dateOriginalStr() {
if (!this.dateOriginal) return null;
return utils.getLongDateStr(this.dateOriginal.toDate(), true);
}
get dateOriginalTime() {
if (!this.dateOriginal) return null;
// Try to get timezone
let tz = this.exif["OffsetTimeOriginal"] || this.exif["OffsetTime"];
tz = tz ? "GMT" + tz : "";
let parts = [];
parts.push(this.dateOriginal.format("h:mm A"));
if (tz) parts.push(tz);
return parts;
}
/** Camera make and model info */
get 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() {
const f = this.exif["FNumber"] || this.exif["Aperture"];
const s = this.shutterSpeed;
const len = this.exif["FocalLength"];
const iso = this.exif["ISO"];
const parts = [];
if (f) parts.push(`f/${f}`);
if (s) parts.push(`${s}`);
if (len) parts.push(`${len}mm`);
if (iso) parts.push(`ISO${iso}`);
return parts;
}
/** Convert shutter speed decimal to 1/x format */
get shutterSpeed() {
const speed = Number(
this.exif["ShutterSpeedValue"] ||
this.exif["ShutterSpeed"] ||
this.exif["ExposureTime"]
);
if (!speed) return null;
if (speed < 1) {
return `1/${Math.round(1 / speed)}`;
} else {
return `${Math.round(speed * 10) / 10}s`;
}
}
/** Image info */
get imageInfo() {
return this.fileInfo.basename || (<any>this.fileInfo).name;
}
get imageInfoSub() {
let parts = [];
let mp = Number(this.exif["Megapixels"]);
if (this.baseInfo.w && this.baseInfo.h) {
parts.push(`${this.baseInfo.w}x${this.baseInfo.h}`);
if (!mp) {
mp = (this.baseInfo.w * this.baseInfo.h) / 1000000;
if (this.dateOriginal) {
list.push({
title: this.dateOriginalStr,
subtitle: this.dateOriginalTime,
icon: CalendarIcon,
edit: () => globalThis.editDate(globalThis.currentViewerPhoto),
});
}
}
if (mp) {
parts.unshift(`${mp.toFixed(1)}MP`);
}
if (this.camera) {
list.push({
title: this.camera,
subtitle: this.cameraSub,
icon: CameraIrisIcon,
});
}
return parts;
}
if (this.imageInfo) {
list.push({
title: this.imageInfo,
subtitle: this.imageInfoSub,
icon: ImageIcon,
});
}
get address() {
if (!this.lat || !this.lon) return null;
const title = this.exif?.["Title"];
const desc = this.exif?.["Description"];
if (title || desc) {
list.push({
title: title || this.t("memories", "No title"),
subtitle: [desc || this.t("memories", "No description")],
icon: InfoIcon,
edit: () => globalThis.editExif(globalThis.currentViewerPhoto),
});
}
if (!this.nominatim) return this.t("memories", "Loading …");
if (this.address) {
list.push({
title: this.address,
subtitle: [],
icon: LocationIcon,
href: this.mapFullUrl,
});
}
const n = this.nominatim;
const country = n.address.country_code?.toUpperCase();
return list;
},
if (n.address?.city && n.address.state) {
return `${n.address.city}, ${n.address.state}, ${country}`;
} else if (n.address?.state) {
return `${n.address.state}, ${country}`;
} else if (n.address?.country) {
return n.address.country;
} else {
return n.display_name;
}
}
/** Date taken info */
dateOriginal() {
const dt = this.exif["DateTimeOriginal"] || this.exif["CreateDate"];
if (!dt) return null;
get lat() {
return this.exif["GPSLatitude"];
}
const m = moment.utc(dt, "YYYY:MM:DD HH:mm:ss");
if (!m.isValid()) return null;
m.locale(getCanonicalLocale());
return m;
},
get lon() {
return this.exif["GPSLongitude"];
}
dateOriginalStr() {
if (!this.dateOriginal) return null;
return utils.getLongDateStr(this.dateOriginal.toDate(), true);
},
get mapUrl() {
const boxSize = 0.0075;
const bbox = [
this.lon - boxSize,
this.lat - boxSize,
this.lon + boxSize,
this.lat + boxSize,
];
const m = `${this.lat},${this.lon}`;
return `https://www.openstreetmap.org/export/embed.html?bbox=${bbox.join()}&marker=${m}`;
}
dateOriginalTime() {
if (!this.dateOriginal) return null;
get mapFullUrl() {
return `https://www.openstreetmap.org/?mlat=${this.lat}&mlon=${this.lon}#map=18/${this.lat}/${this.lon}`;
}
// Try to get timezone
let tz = this.exif["OffsetTimeOriginal"] || this.exif["OffsetTime"];
tz = tz ? "GMT" + tz : "";
async getNominatim() {
const lat = this.lat;
const lon = this.lon;
if (!lat || !lon) return null;
let parts = [];
parts.push(this.dateOriginal.format("h:mm A"));
if (tz) parts.push(tz);
const state = this.state;
const n = await axios.get(
`https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=json&zoom=18`
);
if (state !== this.state) return;
this.nominatim = n.data;
}
}
return parts;
},
/** Camera make and model info */
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}`;
},
cameraSub() {
const f = this.exif["FNumber"] || this.exif["Aperture"];
const s = this.shutterSpeed;
const len = this.exif["FocalLength"];
const iso = this.exif["ISO"];
const parts = [];
if (f) parts.push(`f/${f}`);
if (s) parts.push(`${s}`);
if (len) parts.push(`${len}mm`);
if (iso) parts.push(`ISO${iso}`);
return parts;
},
/** Convert shutter speed decimal to 1/x format */
shutterSpeed() {
const speed = Number(
this.exif["ShutterSpeedValue"] ||
this.exif["ShutterSpeed"] ||
this.exif["ExposureTime"]
);
if (!speed) return null;
if (speed < 1) {
return `1/${Math.round(1 / speed)}`;
} else {
return `${Math.round(speed * 10) / 10}s`;
}
},
/** Image info */
imageInfo() {
return this.fileInfo.basename || (<any>this.fileInfo).name;
},
imageInfoSub() {
let parts = [];
let mp = Number(this.exif["Megapixels"]);
if (this.baseInfo.w && this.baseInfo.h) {
parts.push(`${this.baseInfo.w}x${this.baseInfo.h}`);
if (!mp) {
mp = (this.baseInfo.w * this.baseInfo.h) / 1000000;
}
}
if (mp) {
parts.unshift(`${mp.toFixed(1)}MP`);
}
return parts;
},
address() {
if (!this.lat || !this.lon) return null;
if (!this.nominatim) return this.t("memories", "Loading …");
const n = this.nominatim;
const country = n.address.country_code?.toUpperCase();
if (n.address?.city && n.address.state) {
return `${n.address.city}, ${n.address.state}, ${country}`;
} else if (n.address?.state) {
return `${n.address.state}, ${country}`;
} else if (n.address?.country) {
return n.address.country;
} else {
return n.display_name;
}
},
lat() {
return this.exif["GPSLatitude"];
},
lon() {
return this.exif["GPSLongitude"];
},
mapUrl() {
const boxSize = 0.0075;
const bbox = [
this.lon - boxSize,
this.lat - boxSize,
this.lon + boxSize,
this.lat + boxSize,
];
const m = `${this.lat},${this.lon}`;
return `https://www.openstreetmap.org/export/embed.html?bbox=${bbox.join()}&marker=${m}`;
},
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;
const lon = this.lon;
if (!lat || !lon) return null;
const state = this.state;
const n = await axios.get(
`https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=json&zoom=18`
);
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,458 +58,468 @@ 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) {
/** Rows from Timeline */
@Prop() rows!: IRow[];
/** Total height */
@Prop() height!: number;
/** Actual recycler component */
@Prop() recycler!: any;
/** Recycler before slot component */
@Prop() recyclerBefore!: any;
/** Last known height at adjustment */
private lastAdjustHeight = 0;
/** Height of the entire photo view */
private recyclerHeight: number = 100;
/** Rect of scroller */
private scrollerRect: DOMRect = null;
/** Computed ticks */
private ticks: ITick[] = [];
/** Computed cursor top */
private cursorY = 0;
/** Hover cursor top */
private hoverCursorY = -5;
/** Hover cursor text */
private hoverCursorText = "";
/** Scrolling using the scroller */
private scrollingTimer = 0;
/** Scrolling now using the scroller */
private scrollingNowTimer = 0;
/** Scrolling recycler */
private scrollingRecyclerTimer = 0;
/** Scrolling recycler now */
private scrollingRecyclerNowTimer = 0;
/** Recycler scrolling throttle */
private scrollingRecyclerUpdateTimer = 0;
/** View size reflow timer */
private reflowRequest = false;
/** Tick adjust timer */
private adjustRequest = false;
/** Scroller is being moved with interaction */
private interacting = false;
/** Track the last requested y position when interacting */
private lastRequestedRecyclerY = 0;
props: {
/** Rows from Timeline */
rows: Array as PropType<IRow[]>,
/** Total height */
height: Number,
/** Actual recycler component */
recycler: Object,
/** Recycler before slot component */
recyclerBefore: HTMLDivElement,
},
/** Get the visible ticks */
get visibleTicks() {
let key = 9999999900;
return this.ticks
.filter((tick) => tick.s)
.map((tick) => {
if (tick.text) {
tick.key = key = tick.dayId * 100;
} else {
tick.key = ++key; // days are sorted descending
}
return tick;
});
}
/** Reset state */
public reset() {
this.ticks = [];
this.cursorY = 0;
this.hoverCursorY = -5;
this.hoverCursorText = "";
this.reflowRequest = false;
// Clear all timers
clearTimeout(this.scrollingTimer);
clearTimeout(this.scrollingNowTimer);
clearTimeout(this.scrollingRecyclerTimer);
clearTimeout(this.scrollingRecyclerNowTimer);
clearTimeout(this.scrollingRecyclerUpdateTimer);
this.scrollingTimer = 0;
this.scrollingNowTimer = 0;
this.scrollingRecyclerTimer = 0;
this.scrollingRecyclerNowTimer = 0;
this.scrollingRecyclerUpdateTimer = 0;
}
/** Recycler scroll event, must be called by timeline */
public recyclerScrolled() {
// This isn't a renewing timer, it's a scheduled task
if (this.scrollingRecyclerUpdateTimer) return;
this.scrollingRecyclerUpdateTimer = window.setTimeout(() => {
this.scrollingRecyclerUpdateTimer = 0;
this.updateFromRecyclerScroll();
}, 100);
// 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() {
// Ignore if not initialized or moving
if (!this.ticks.length || this.interacting) return;
// Get the scroll position
const scroll = this.recycler?.$el?.scrollTop || 0;
// Get cursor px position
const { top1, top2, y1, y2 } = this.getCoords(scroll, "y");
const topfrac = (scroll - y1) / (y2 - y1);
const rtop = top1 + (top2 - top1) * (topfrac || 0);
// Always move static cursor to right position
this.cursorY = rtop;
// Move hover cursor to same position unless hovering
// Regardless, we need this call because the internal mapping might have changed
if ((<HTMLElement>this.$refs.scroller).matches(":hover")) {
this.moveHoverCursor(this.hoverCursorY);
} else {
this.moveHoverCursor(rtop);
}
}
/** Re-create tick data in the next frame */
public async reflow() {
if (this.reflowRequest) return;
this.reflowRequest = true;
await this.$nextTick();
this.reflowNow();
this.reflowRequest = false;
}
/** Re-create tick data */
private reflowNow() {
// Ignore if not initialized
if (!this.recycler?.$refs.wrapper) return;
// Refresh height of recycler
this.recyclerHeight = this.recycler.$refs.wrapper.clientHeight;
// Recreate ticks data
this.recreate();
// Adjust top
this.adjustNow();
}
/** Recreate from scratch */
private recreate() {
// Clear and override any adjust timer
this.ticks = [];
// Ticks
let prevYear = 9999;
let prevMonth = 0;
// Get a new tick
const getTick = (
dayId: number,
isMonth = false,
text?: string | number
): ITick => {
return {
dayId,
isMonth,
text,
y: 0,
count: 0,
topF: 0,
top: 0,
s: false,
};
data() {
return {
/** Last known height at adjustment */
lastAdjustHeight: 0,
/** Height of the entire photo view */
recyclerHeight: 100,
/** Rect of scroller */
scrollerRect: null as DOMRect,
/** Computed ticks */
ticks: [] as ITick[],
/** Computed cursor top */
cursorY: 0,
/** Hover cursor top */
hoverCursorY: -5,
/** Hover cursor text */
hoverCursorText: "",
/** Scrolling using the scroller */
scrollingTimer: 0,
/** Scrolling now using the scroller */
scrollingNowTimer: 0,
/** Scrolling recycler */
scrollingRecyclerTimer: 0,
/** Scrolling recycler now */
scrollingRecyclerNowTimer: 0,
/** Recycler scrolling throttle */
scrollingRecyclerUpdateTimer: 0,
/** View size reflow timer */
reflowRequest: false,
/** Tick adjust timer */
adjustRequest: false,
/** Scroller is being moved with interaction */
interacting: false,
/** Track the last requested y position when interacting */
lastRequestedRecyclerY: 0,
};
},
// Iterate over rows
for (const row of this.rows) {
if (row.type === IRowType.HEAD) {
// Create tick
if (this.TagDayIDValueSet.has(row.dayId)) {
// Blank tick
this.ticks.push(getTick(row.dayId));
} else {
// Make date string
const dateTaken = utils.dayIdToDate(row.dayId);
computed: {
/** Get the visible ticks */
visibleTicks() {
let key = 9999999900;
return this.ticks
.filter((tick) => tick.s)
.map((tick) => {
if (tick.text) {
tick.key = key = tick.dayId * 100;
} else {
tick.key = ++key; // days are sorted descending
}
return tick;
});
},
},
methods: {
/** Reset state */
reset() {
this.ticks = [];
this.cursorY = 0;
this.hoverCursorY = -5;
this.hoverCursorText = "";
this.reflowRequest = false;
// Clear all timers
clearTimeout(this.scrollingTimer);
clearTimeout(this.scrollingNowTimer);
clearTimeout(this.scrollingRecyclerTimer);
clearTimeout(this.scrollingRecyclerNowTimer);
clearTimeout(this.scrollingRecyclerUpdateTimer);
this.scrollingTimer = 0;
this.scrollingNowTimer = 0;
this.scrollingRecyclerTimer = 0;
this.scrollingRecyclerNowTimer = 0;
this.scrollingRecyclerUpdateTimer = 0;
},
/** Recycler scroll event, must be called by timeline */
recyclerScrolled() {
// This isn't a renewing timer, it's a scheduled task
if (this.scrollingRecyclerUpdateTimer) return;
this.scrollingRecyclerUpdateTimer = window.setTimeout(() => {
this.scrollingRecyclerUpdateTimer = 0;
this.updateFromRecyclerScroll();
}, 100);
// 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 */
updateFromRecyclerScroll() {
// Ignore if not initialized or moving
if (!this.ticks.length || this.interacting) return;
// Get the scroll position
const scroll = this.recycler?.$el?.scrollTop || 0;
// Get cursor px position
const { top1, top2, y1, y2 } = this.getCoords(scroll, "y");
const topfrac = (scroll - y1) / (y2 - y1);
const rtop = top1 + (top2 - top1) * (topfrac || 0);
// Always move static cursor to right position
this.cursorY = rtop;
// Move hover cursor to same position unless hovering
// Regardless, we need this call because the internal mapping might have changed
if ((<HTMLElement>this.$refs.scroller).matches(":hover")) {
this.moveHoverCursor(this.hoverCursorY);
} else {
this.moveHoverCursor(rtop);
}
},
/** Re-create tick data in the next frame */
async reflow() {
if (this.reflowRequest) return;
this.reflowRequest = true;
await this.$nextTick();
this.reflowNow();
this.reflowRequest = false;
},
/** Re-create tick data */
reflowNow() {
// Ignore if not initialized
if (!this.recycler?.$refs.wrapper) return;
// Refresh height of recycler
this.recyclerHeight = this.recycler.$refs.wrapper.clientHeight;
// Recreate ticks data
this.recreate();
// Adjust top
this.adjustNow();
},
/** Recreate from scratch */
recreate() {
// Clear and override any adjust timer
this.ticks = [];
// Ticks
let prevYear = 9999;
let prevMonth = 0;
// Get a new tick
const getTick = (
dayId: number,
isMonth = false,
text?: string | number
): ITick => {
return {
dayId,
isMonth,
text,
y: 0,
count: 0,
topF: 0,
top: 0,
s: false,
};
};
// Iterate over rows
for (const row of this.rows) {
if (row.type === IRowType.HEAD) {
// Create tick
const dtYear = dateTaken.getUTCFullYear();
const dtMonth = dateTaken.getUTCMonth();
const isMonth = dtMonth !== prevMonth || dtYear !== prevYear;
const text = dtYear === prevYear ? undefined : dtYear;
this.ticks.push(getTick(row.dayId, isMonth, text));
if (this.TagDayIDValueSet.has(row.dayId)) {
// Blank tick
this.ticks.push(getTick(row.dayId));
} else {
// Make date string
const dateTaken = utils.dayIdToDate(row.dayId);
prevMonth = dtMonth;
prevYear = dtYear;
// Create tick
const dtYear = dateTaken.getUTCFullYear();
const dtMonth = dateTaken.getUTCMonth();
const isMonth = dtMonth !== prevMonth || dtYear !== prevYear;
const text = dtYear === prevYear ? undefined : dtYear;
this.ticks.push(getTick(row.dayId, isMonth, text));
prevMonth = dtMonth;
prevYear = dtYear;
}
}
}
}
}
},
/**
* Update tick positions without truncating the list
* This is much cheaper than reflowing the whole thing
*/
public async adjust() {
if (this.adjustRequest) return;
this.adjustRequest = true;
await this.$nextTick();
this.adjustNow();
this.adjustRequest = false;
}
/**
* Update tick positions without truncating the list
* This is much cheaper than reflowing the whole thing
*/
async adjust() {
if (this.adjustRequest) return;
this.adjustRequest = true;
await this.$nextTick();
this.adjustNow();
this.adjustRequest = false;
},
/** Do adjustment synchronously */
private adjustNow() {
// Refresh height of recycler
this.recyclerHeight = this.recycler.$refs.wrapper.clientHeight;
const extraY = this.recyclerBefore?.clientHeight || 0;
/** Do adjustment synchronously */
adjustNow() {
// Refresh height of recycler
this.recyclerHeight = this.recycler.$refs.wrapper.clientHeight;
const extraY = this.recyclerBefore?.clientHeight || 0;
// Start with the first tick. Walk over all rows counting the
// y position. When you hit a row with the tick, update y and
// top values and move to the next tick.
let tickId = 0;
let y = extraY;
let count = 0;
// Start with the first tick. Walk over all rows counting the
// y position. When you hit a row with the tick, update y and
// top values and move to the next tick.
let tickId = 0;
let y = extraY;
let count = 0;
// We only need to recompute top and visible ticks if count
// of some tick has changed.
let needRecomputeTop = false;
// We only need to recompute top and visible ticks if count
// of some tick has changed.
let needRecomputeTop = false;
// Check if height changed
if (this.lastAdjustHeight !== this.height) {
needRecomputeTop = true;
this.lastAdjustHeight = this.height;
}
for (const row of this.rows) {
// Check if tick is valid
if (tickId >= this.ticks.length) break;
// Check if we hit the next tick
const tick = this.ticks[tickId];
if (tick.dayId === row.dayId) {
tick.y = y;
// Check if count has changed
needRecomputeTop ||= tick.count !== count;
tick.count = count;
// Move to next tick
count += row.day.count;
tickId++;
// Check if height changed
if (this.lastAdjustHeight !== this.height) {
needRecomputeTop = true;
this.lastAdjustHeight = this.height;
}
y += row.size;
}
for (const row of this.rows) {
// Check if tick is valid
if (tickId >= this.ticks.length) break;
// Compute visible ticks
if (needRecomputeTop) {
this.setTicksTop(count);
this.computeVisibleTicks();
}
}
// Check if we hit the next tick
const tick = this.ticks[tickId];
if (tick.dayId === row.dayId) {
tick.y = y;
/** Mark ticks as visible or invisible */
private computeVisibleTicks() {
// Kind of unrelated here, but refresh rect
this.scrollerRect = (
this.$refs.scroller as HTMLElement
).getBoundingClientRect();
// Check if count has changed
needRecomputeTop ||= tick.count !== count;
tick.count = count;
// Do another pass to figure out which points are visible
// This is not as bad as it looks, it's actually 12*O(n)
// because there are only 12 months in a year
const fontSizePx = parseFloat(
getComputedStyle(this.$refs.cursorSt as any).fontSize
);
const minGap = fontSizePx + (globalThis.windowInnerWidth <= 768 ? 5 : 2);
let prevShow = -9999;
for (const [idx, tick] of this.ticks.entries()) {
// Conservative
tick.s = false;
// These aren't for showing
if (!tick.isMonth) continue;
// You can't see these anyway, why bother?
if (tick.top < minGap || tick.top > this.height - minGap) continue;
// Will overlap with the previous tick. Skip anyway.
if (tick.top - prevShow < minGap) continue;
// This is a labelled tick then show it anyway for the sake of best effort
if (tick.text) {
prevShow = tick.top;
tick.s = true;
continue;
}
// Lookahead for next labelled tick
// If showing this tick would overlap the next one, don't show this one
let i = idx + 1;
while (i < this.ticks.length) {
if (this.ticks[i].text) {
break;
// Move to next tick
count += row.day.count;
tickId++;
}
i++;
y += row.size;
}
if (i < this.ticks.length) {
// A labelled tick was found
const nextLabelledTick = this.ticks[i];
if (
tick.top + minGap > nextLabelledTick.top &&
nextLabelledTick.top < this.height - minGap
) {
// make sure this will be shown
// Compute visible ticks
if (needRecomputeTop) {
this.setTicksTop(count);
this.computeVisibleTicks();
}
},
/** Mark ticks as visible or invisible */
computeVisibleTicks() {
// Kind of unrelated here, but refresh rect
this.scrollerRect = (
this.$refs.scroller as HTMLElement
).getBoundingClientRect();
// Do another pass to figure out which points are visible
// This is not as bad as it looks, it's actually 12*O(n)
// because there are only 12 months in a year
const fontSizePx = parseFloat(
getComputedStyle(this.$refs.cursorSt as any).fontSize
);
const minGap = fontSizePx + (globalThis.windowInnerWidth <= 768 ? 5 : 2);
let prevShow = -9999;
for (const [idx, tick] of this.ticks.entries()) {
// Conservative
tick.s = false;
// These aren't for showing
if (!tick.isMonth) continue;
// You can't see these anyway, why bother?
if (tick.top < minGap || tick.top > this.height - minGap) continue;
// Will overlap with the previous tick. Skip anyway.
if (tick.top - prevShow < minGap) continue;
// This is a labelled tick then show it anyway for the sake of best effort
if (tick.text) {
prevShow = tick.top;
tick.s = true;
continue;
}
// Lookahead for next labelled tick
// If showing this tick would overlap the next one, don't show this one
let i = idx + 1;
while (i < this.ticks.length) {
if (this.ticks[i].text) {
break;
}
i++;
}
if (i < this.ticks.length) {
// A labelled tick was found
const nextLabelledTick = this.ticks[i];
if (
tick.top + minGap > nextLabelledTick.top &&
nextLabelledTick.top < this.height - minGap
) {
// make sure this will be shown
continue;
}
}
// Show this tick
tick.s = true;
prevShow = tick.top;
}
},
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 */
moveHoverCursor(y: number) {
this.hoverCursorY = y;
// Get index of previous tick
let idx = utils.binarySearch(this.ticks, y, "topF");
if (idx === 0) {
// use this tick
} else if (idx >= 1 && idx <= this.ticks.length) {
idx = idx - 1;
} else {
return;
}
// Show this tick
tick.s = true;
prevShow = tick.top;
}
}
// DayId of current hover
const dayId = this.ticks[idx]?.dayId;
private setTicksTop(total: number) {
for (const tick of this.ticks) {
tick.topF = this.height * (tick.count / total);
tick.top = utils.roundHalf(tick.topF);
}
}
// Special days
if (dayId === undefined || this.TagDayIDValueSet.has(dayId)) {
this.hoverCursorText = "";
return;
}
/** Change actual position of the hover cursor */
private moveHoverCursor(y: number) {
this.hoverCursorY = y;
const date = utils.dayIdToDate(dayId);
this.hoverCursorText = utils.getShortDateStr(date);
},
// Get index of previous tick
let idx = utils.binarySearch(this.ticks, y, "topF");
if (idx === 0) {
// use this tick
} else if (idx >= 1 && idx <= this.ticks.length) {
idx = idx - 1;
} else {
return;
}
/** Handle mouse hover */
mousemove(event: MouseEvent) {
if (event.buttons) {
this.mousedown(event);
}
this.moveHoverCursor(event.offsetY);
},
// DayId of current hover
const dayId = this.ticks[idx]?.dayId;
/** Handle mouse leave */
mouseleave() {
this.interactend();
this.moveHoverCursor(this.cursorY);
},
// Special days
if (dayId === undefined || this.TagDayIDValueSet.has(dayId)) {
this.hoverCursorText = "";
return;
}
/** Binary search and get coords surrounding position */
getCoords(y: number, field: "topF" | "y") {
// Top of first and second ticks
let top1 = 0,
top2 = 0,
y1 = 0,
y2 = 0;
const date = utils.dayIdToDate(dayId);
this.hoverCursorText = utils.getShortDateStr(date);
}
// Get index of previous tick
let idx = utils.binarySearch(this.ticks, y, field);
if (idx <= 0) {
top1 = 0;
top2 = this.ticks[0].topF;
y1 = 0;
y2 = this.ticks[0].y;
} else if (idx >= this.ticks.length) {
const t = this.ticks[this.ticks.length - 1];
top1 = t.topF;
top2 = this.height;
y1 = t.y;
y2 = this.recyclerHeight;
} else {
const t1 = this.ticks[idx - 1];
const t2 = this.ticks[idx];
top1 = t1.topF;
top2 = t2.topF;
y1 = t1.y;
y2 = t2.y;
}
/** Handle mouse hover */
private mousemove(event: MouseEvent) {
if (event.buttons) {
this.mousedown(event);
}
this.moveHoverCursor(event.offsetY);
}
return { top1, top2, y1, y2 };
},
/** Handle mouse leave */
private mouseleave() {
this.interactend();
this.moveHoverCursor(this.cursorY);
}
/** Move to given scroller Y */
moveto(y: number, snap: boolean) {
// Move cursor immediately to prevent jank
this.cursorY = y;
this.hoverCursorY = y;
/** Binary search and get coords surrounding position */
private getCoords(y: number, field: "topF" | "y") {
// Top of first and second ticks
let top1 = 0,
top2 = 0,
y1 = 0,
y2 = 0;
const { top1, top2, y1, y2 } = this.getCoords(y, "topF");
const yfrac = (y - top1) / (top2 - top1);
const ry = y1 + (y2 - y1) * (yfrac || 0);
const targetY = snap ? y1 + SNAP_OFFSET : ry;
// Get index of previous tick
let idx = utils.binarySearch(this.ticks, y, field);
if (idx <= 0) {
top1 = 0;
top2 = this.ticks[0].topF;
y1 = 0;
y2 = this.ticks[0].y;
} else if (idx >= this.ticks.length) {
const t = this.ticks[this.ticks.length - 1];
top1 = t.topF;
top2 = this.height;
y1 = t.y;
y2 = this.recyclerHeight;
} else {
const t1 = this.ticks[idx - 1];
const t2 = this.ticks[idx];
top1 = t1.topF;
top2 = t2.topF;
y1 = t1.y;
y2 = t2.y;
}
if (this.lastRequestedRecyclerY !== targetY) {
this.lastRequestedRecyclerY = targetY;
this.recycler.scrollToPosition(targetY);
}
return { top1, top2, y1, y2 };
}
this.handleScroll();
},
/** Move to given scroller Y */
private moveto(y: number, snap: boolean) {
// Move cursor immediately to prevent jank
this.cursorY = y;
this.hoverCursorY = y;
/** Handle mouse click */
mousedown(event: MouseEvent) {
this.interactstart(); // end called on mouseup
this.moveto(event.offsetY, false);
},
const { top1, top2, y1, y2 } = this.getCoords(y, "topF");
const yfrac = (y - top1) / (top2 - top1);
const ry = y1 + (y2 - y1) * (yfrac || 0);
const targetY = snap ? y1 + SNAP_OFFSET : ry;
/** Handle touch */
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);
},
if (this.lastRequestedRecyclerY !== targetY) {
this.lastRequestedRecyclerY = targetY;
this.recycler.scrollToPosition(targetY);
}
interactstart() {
this.interacting = true;
},
this.handleScroll();
}
interactend() {
this.interacting = false;
this.recyclerScrolled(); // make sure final position is correct
},
/** Handle mouse click */
private mousedown(event: MouseEvent) {
this.interactstart(); // end called on mouseup
this.moveto(event.offsetY, false);
}
/** Handle touch */
private 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() {
this.interacting = true;
}
private interactend() {
this.interacting = false;
this.recyclerScrolled(); // make sure final position is correct
}
/** Update scroller is being used to scroll recycler */
private handleScroll() {
utils.setRenewingTimeout(this, "scrollingNowTimer", null, 200);
utils.setRenewingTimeout(this, "scrollingTimer", null, 1500);
}
}
/** Update scroller is being used to scroll recycler */
handleScroll() {
utils.setRenewingTimeout(this, "scrollingNowTimer", null, 200);
utils.setRenewingTimeout(this, "scrollingTimer", null, 1500);
},
},
});
</script>
<style lang="scss" scoped>

File diff suppressed because it is too large Load Diff

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,62 +77,69 @@ 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");
}
async chooseFolder(title: string, initial: string) {
const picker = getFilePickerBuilder(title)
.setMultiSelect(false)
.setModal(true)
.setType(1)
.addMimeTypeFilter("httpd/unix-directory")
.allowDirectories()
.startAt(initial)
.build();
computed: {
pathSelTitle() {
return this.t("memories", "Choose Timeline Paths");
},
},
return await picker.pick();
}
methods: {
async chooseFolder(title: string, initial: string) {
const picker = getFilePickerBuilder(title)
.setMultiSelect(false)
.setModal(true)
.setType(1)
.addMimeTypeFilter("httpd/unix-directory")
.allowDirectories()
.startAt(initial)
.build();
async chooseTimelinePath() {
(<any>this.$refs.multiPathModal).open(this.config_timelinePath.split(";"));
}
return await picker.pick();
},
async saveTimelinePath(paths: string[]) {
if (!paths || !paths.length) return;
async chooseTimelinePath() {
(<any>this.$refs.multiPathModal).open(
this.config_timelinePath.split(";")
);
},
const newPath = paths.join(";");
if (newPath !== this.config_timelinePath) {
this.config_timelinePath = newPath;
await this.updateSetting("timelinePath");
}
}
async saveTimelinePath(paths: string[]) {
if (!paths || !paths.length) return;
async chooseFoldersPath() {
let newPath = await this.chooseFolder(
this.t("memories", "Choose the root for the folders view"),
this.config_foldersPath
);
if (newPath === "") newPath = "/";
if (newPath !== this.config_foldersPath) {
this.config_foldersPath = newPath;
await this.updateSetting("foldersPath");
}
}
const newPath = paths.join(";");
if (newPath !== this.config_timelinePath) {
this.config_timelinePath = newPath;
await this.updateSetting("timelinePath");
}
},
async updateSquareThumbs() {
await this.updateSetting("squareThumbs");
}
async chooseFoldersPath() {
let newPath = await this.chooseFolder(
this.t("memories", "Choose the root for the folders view"),
this.config_foldersPath
);
if (newPath === "") newPath = "/";
if (newPath !== this.config_foldersPath) {
this.config_foldersPath = newPath;
await this.updateSetting("foldersPath");
}
},
async updateShowHidden() {
await this.updateSetting("showHidden");
}
}
async updateSquareThumbs() {
await this.updateSetting("squareThumbs");
},
async updateShowHidden() {
await this.updateSetting("showHidden");
},
},
});
</script>

File diff suppressed because it is too large Load Diff

View File

@ -29,82 +29,89 @@
</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;
// Separate property because the one on data isn't reactive
private previews: IPhoto[] = [];
props: {
data: Object as PropType<IFolder>,
},
// Error occured fetching thumbs
private error = false;
data() {
return {
// Separate property because the one on data isn't reactive
previews: [] as IPhoto[],
// Error occured fetching thumbs
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() {
this.refreshPreviews();
}
watch: {
data() {
this.refreshPreviews();
},
},
/** Refresh previews */
refreshPreviews() {
// Reset state
this.error = false;
methods: {
/** Refresh previews */
refreshPreviews() {
// Reset state
this.error = false;
// Check if valid path present
if (!this.data.path) {
this.error = true;
return;
}
// Get preview infos
const previews = this.data.previews;
if (previews) {
if (previews.length > 0 && previews.length < 4) {
this.previews = [previews[0]];
} else {
this.previews = previews.slice(0, 4);
// Check if valid path present
if (!this.data.path) {
this.error = true;
return;
}
}
}
/** 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 } };
}
}
// Get preview infos
const previews = this.data.previews;
if (previews) {
if (previews.length > 0 && previews.length < 4) {
this.previews = [previews[0]];
} else {
this.previews = previews.slice(0, 4);
}
}
},
},
});
</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