remove class vue dep (1)
parent
8520d0dc1e
commit
07379d836c
326
src/App.vue
326
src/App.vue
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
@ -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
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue