remove class vue dep (1)
parent
8520d0dc1e
commit
07379d836c
238
src/App.vue
238
src/App.vue
|
@ -38,7 +38,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Mixins, Watch } from "vue-property-decorator";
|
import Vue, { defineComponent } from "vue";
|
||||||
|
|
||||||
import NcContent from "@nextcloud/vue/dist/Components/NcContent";
|
import NcContent from "@nextcloud/vue/dist/Components/NcContent";
|
||||||
import NcAppContent from "@nextcloud/vue/dist/Components/NcAppContent";
|
import NcAppContent from "@nextcloud/vue/dist/Components/NcAppContent";
|
||||||
|
@ -49,15 +49,12 @@ const NcAppNavigationSettings = () =>
|
||||||
import("@nextcloud/vue/dist/Components/NcAppNavigationSettings");
|
import("@nextcloud/vue/dist/Components/NcAppNavigationSettings");
|
||||||
|
|
||||||
import { generateUrl } from "@nextcloud/router";
|
import { generateUrl } from "@nextcloud/router";
|
||||||
import { getCurrentUser } from "@nextcloud/auth";
|
|
||||||
import { translate as t } from "@nextcloud/l10n";
|
import { translate as t } from "@nextcloud/l10n";
|
||||||
|
|
||||||
import Timeline from "./components/Timeline.vue";
|
import Timeline from "./components/Timeline.vue";
|
||||||
import Settings from "./components/Settings.vue";
|
import Settings from "./components/Settings.vue";
|
||||||
import FirstStart from "./components/FirstStart.vue";
|
import FirstStart from "./components/FirstStart.vue";
|
||||||
import Metadata from "./components/Metadata.vue";
|
import Metadata from "./components/Metadata.vue";
|
||||||
import GlobalMixin from "./mixins/GlobalMixin";
|
|
||||||
import UserConfig from "./mixins/UserConfig";
|
|
||||||
|
|
||||||
import ImageMultiple from "vue-material-design-icons/ImageMultiple.vue";
|
import ImageMultiple from "vue-material-design-icons/ImageMultiple.vue";
|
||||||
import FolderIcon from "vue-material-design-icons/Folder.vue";
|
import FolderIcon from "vue-material-design-icons/Folder.vue";
|
||||||
|
@ -70,7 +67,8 @@ import PeopleIcon from "vue-material-design-icons/AccountBoxMultiple.vue";
|
||||||
import TagsIcon from "vue-material-design-icons/Tag.vue";
|
import TagsIcon from "vue-material-design-icons/Tag.vue";
|
||||||
import MapIcon from "vue-material-design-icons/Map.vue";
|
import MapIcon from "vue-material-design-icons/Map.vue";
|
||||||
|
|
||||||
@Component({
|
export default defineComponent({
|
||||||
|
name: "App",
|
||||||
components: {
|
components: {
|
||||||
NcContent,
|
NcContent,
|
||||||
NcAppContent,
|
NcAppContent,
|
||||||
|
@ -93,13 +91,109 @@ import MapIcon from "vue-material-design-icons/Map.vue";
|
||||||
TagsIcon,
|
TagsIcon,
|
||||||
MapIcon,
|
MapIcon,
|
||||||
},
|
},
|
||||||
})
|
|
||||||
export default class App extends Mixins(GlobalMixin, UserConfig) {
|
|
||||||
// Outer element
|
|
||||||
|
|
||||||
private metadataComponent!: Metadata;
|
data() {
|
||||||
|
return {
|
||||||
|
navItems: [],
|
||||||
|
metadataComponent: null as Metadata,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
private readonly navItemsAll = (self: typeof this) => [
|
computed: {
|
||||||
|
ncVersion() {
|
||||||
|
const version = (<any>window.OC).config.version.split(".");
|
||||||
|
return Number(version[0]);
|
||||||
|
},
|
||||||
|
recognize() {
|
||||||
|
if (!this.config_recognizeEnabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.config_facerecognitionInstalled) {
|
||||||
|
return t("memories", "People (Recognize)");
|
||||||
|
}
|
||||||
|
|
||||||
|
return t("memories", "People");
|
||||||
|
},
|
||||||
|
facerecognition() {
|
||||||
|
if (!this.config_facerecognitionInstalled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.config_recognizeEnabled) {
|
||||||
|
return t("memories", "People (Face Recognition)");
|
||||||
|
}
|
||||||
|
|
||||||
|
return t("memories", "People");
|
||||||
|
},
|
||||||
|
isFirstStart() {
|
||||||
|
return this.config_timelinePath === "EMPTY";
|
||||||
|
},
|
||||||
|
showAlbums() {
|
||||||
|
return this.config_albumsEnabled;
|
||||||
|
},
|
||||||
|
removeOuterGap() {
|
||||||
|
return this.ncVersion >= 25;
|
||||||
|
},
|
||||||
|
showNavigation() {
|
||||||
|
return this.$route.name !== "folder-share";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
route() {
|
||||||
|
this.doRouteChecks();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.doRouteChecks();
|
||||||
|
|
||||||
|
// Populate navigation
|
||||||
|
this.navItems = this.navItemsAll().filter(
|
||||||
|
(item) => typeof item.if === "undefined" || Boolean(item.if)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Store CSS variables modified
|
||||||
|
const root = document.documentElement;
|
||||||
|
const colorPrimary =
|
||||||
|
getComputedStyle(root).getPropertyValue("--color-primary");
|
||||||
|
root.style.setProperty("--color-primary-select-light", `${colorPrimary}40`);
|
||||||
|
root.style.setProperty("--plyr-color-main", colorPrimary);
|
||||||
|
|
||||||
|
// Register sidebar metadata tab
|
||||||
|
const OCA = globalThis.OCA;
|
||||||
|
if (OCA.Files && OCA.Files.Sidebar) {
|
||||||
|
OCA.Files.Sidebar.registerTab(
|
||||||
|
new OCA.Files.Sidebar.Tab({
|
||||||
|
id: "memories-metadata",
|
||||||
|
name: this.t("memories", "EXIF"),
|
||||||
|
icon: "icon-details",
|
||||||
|
|
||||||
|
async mount(el, fileInfo, context) {
|
||||||
|
if (this.metadataComponent) {
|
||||||
|
this.metadataComponent.$destroy();
|
||||||
|
}
|
||||||
|
this.metadataComponent = new Vue(Metadata);
|
||||||
|
// Only mount after we have all the info we need
|
||||||
|
await this.metadataComponent.update(fileInfo);
|
||||||
|
this.metadataComponent.$mount(el);
|
||||||
|
},
|
||||||
|
update(fileInfo) {
|
||||||
|
this.metadataComponent.update(fileInfo);
|
||||||
|
},
|
||||||
|
destroy() {
|
||||||
|
this.metadataComponent.$destroy();
|
||||||
|
this.metadataComponent = null;
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
navItemsAll() {
|
||||||
|
return [
|
||||||
{
|
{
|
||||||
name: "timeline",
|
name: "timeline",
|
||||||
icon: ImageMultiple,
|
icon: ImageMultiple,
|
||||||
|
@ -124,19 +218,19 @@ export default class App extends Mixins(GlobalMixin, UserConfig) {
|
||||||
name: "albums",
|
name: "albums",
|
||||||
icon: AlbumIcon,
|
icon: AlbumIcon,
|
||||||
title: t("memories", "Albums"),
|
title: t("memories", "Albums"),
|
||||||
if: self.showAlbums,
|
if: this.showAlbums,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "recognize",
|
name: "recognize",
|
||||||
icon: PeopleIcon,
|
icon: PeopleIcon,
|
||||||
title: self.recognize,
|
title: this.recognize,
|
||||||
if: self.recognize,
|
if: this.recognize,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "facerecognition",
|
name: "facerecognition",
|
||||||
icon: PeopleIcon,
|
icon: PeopleIcon,
|
||||||
title: self.facerecognition,
|
title: this.facerecognition,
|
||||||
if: self.facerecognition,
|
if: this.facerecognition,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "archive",
|
name: "archive",
|
||||||
|
@ -152,115 +246,16 @@ export default class App extends Mixins(GlobalMixin, UserConfig) {
|
||||||
name: "tags",
|
name: "tags",
|
||||||
icon: TagsIcon,
|
icon: TagsIcon,
|
||||||
title: t("memories", "Tags"),
|
title: t("memories", "Tags"),
|
||||||
if: self.config_tagsEnabled,
|
if: this.config_tagsEnabled,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "maps",
|
name: "maps",
|
||||||
icon: MapIcon,
|
icon: MapIcon,
|
||||||
title: t("memories", "Maps"),
|
title: t("memories", "Maps"),
|
||||||
if: self.config_mapsEnabled,
|
if: this.config_mapsEnabled,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
private navItems = [];
|
|
||||||
|
|
||||||
get ncVersion() {
|
|
||||||
const version = (<any>window.OC).config.version.split(".");
|
|
||||||
return Number(version[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
get recognize() {
|
|
||||||
if (!this.config_recognizeEnabled) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.config_facerecognitionInstalled) {
|
|
||||||
return t("memories", "People (Recognize)");
|
|
||||||
}
|
|
||||||
|
|
||||||
return t("memories", "People");
|
|
||||||
}
|
|
||||||
|
|
||||||
get facerecognition() {
|
|
||||||
if (!this.config_facerecognitionInstalled) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.config_recognizeEnabled) {
|
|
||||||
return t("memories", "People (Face Recognition)");
|
|
||||||
}
|
|
||||||
|
|
||||||
return t("memories", "People");
|
|
||||||
}
|
|
||||||
|
|
||||||
get isFirstStart() {
|
|
||||||
return this.config_timelinePath === "EMPTY";
|
|
||||||
}
|
|
||||||
|
|
||||||
get showAlbums() {
|
|
||||||
return this.config_albumsEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
get removeOuterGap() {
|
|
||||||
return this.ncVersion >= 25;
|
|
||||||
}
|
|
||||||
|
|
||||||
get showNavigation() {
|
|
||||||
return this.$route.name !== "folder-share";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Watch("$route")
|
|
||||||
routeChanged() {
|
|
||||||
this.doRouteChecks();
|
|
||||||
}
|
|
||||||
|
|
||||||
mounted() {
|
|
||||||
this.doRouteChecks();
|
|
||||||
|
|
||||||
// Populate navigation
|
|
||||||
this.navItems = this.navItemsAll(this).filter(
|
|
||||||
(item) => typeof item.if === "undefined" || Boolean(item.if)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Store CSS variables modified
|
|
||||||
const root = document.documentElement;
|
|
||||||
const colorPrimary =
|
|
||||||
getComputedStyle(root).getPropertyValue("--color-primary");
|
|
||||||
root.style.setProperty("--color-primary-select-light", `${colorPrimary}40`);
|
|
||||||
root.style.setProperty("--plyr-color-main", colorPrimary);
|
|
||||||
|
|
||||||
// Register sidebar metadata tab
|
|
||||||
const OCA = globalThis.OCA;
|
|
||||||
if (OCA.Files && OCA.Files.Sidebar) {
|
|
||||||
OCA.Files.Sidebar.registerTab(
|
|
||||||
new OCA.Files.Sidebar.Tab({
|
|
||||||
id: "memories-metadata",
|
|
||||||
name: this.t("memories", "EXIF"),
|
|
||||||
icon: "icon-details",
|
|
||||||
|
|
||||||
async mount(el, fileInfo, context) {
|
|
||||||
if (this.metadataComponent) {
|
|
||||||
this.metadataComponent.$destroy();
|
|
||||||
}
|
|
||||||
this.metadataComponent = new Metadata({
|
|
||||||
// Better integration with vue parent component
|
|
||||||
parent: context,
|
|
||||||
});
|
|
||||||
// Only mount after we have all the info we need
|
|
||||||
await this.metadataComponent.update(fileInfo);
|
|
||||||
this.metadataComponent.$mount(el);
|
|
||||||
},
|
},
|
||||||
update(fileInfo) {
|
|
||||||
this.metadataComponent.update(fileInfo);
|
|
||||||
},
|
|
||||||
destroy() {
|
|
||||||
this.metadataComponent.$destroy();
|
|
||||||
this.metadataComponent = null;
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async beforeMount() {
|
async beforeMount() {
|
||||||
if ("serviceWorker" in navigator) {
|
if ("serviceWorker" in navigator) {
|
||||||
|
@ -279,18 +274,18 @@ export default class App extends Mixins(GlobalMixin, UserConfig) {
|
||||||
} else {
|
} else {
|
||||||
console.debug("Service Worker is not enabled on this browser.");
|
console.debug("Service Worker is not enabled on this browser.");
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
linkClick() {
|
linkClick() {
|
||||||
const nav: any = this.$refs.nav;
|
const nav: any = this.$refs.nav;
|
||||||
if (globalThis.windowInnerWidth <= 1024) nav?.toggleNavigation(false);
|
if (globalThis.windowInnerWidth <= 1024) nav?.toggleNavigation(false);
|
||||||
}
|
},
|
||||||
|
|
||||||
doRouteChecks() {
|
doRouteChecks() {
|
||||||
if (this.$route.name === "folder-share") {
|
if (this.$route.name === "folder-share") {
|
||||||
this.putFolderShareToken(this.$route.params.token);
|
this.putFolderShareToken(this.$route.params.token);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
putFolderShareToken(token: string) {
|
putFolderShareToken(token: string) {
|
||||||
// Viewer looks for an input with ID sharingToken with the value as the token
|
// Viewer looks for an input with ID sharingToken with the value as the token
|
||||||
|
@ -308,8 +303,9 @@ export default class App extends Mixins(GlobalMixin, UserConfig) {
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenInput.value = token;
|
tokenInput.value = token;
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|
|
@ -47,7 +47,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Mixins } from "vue-property-decorator";
|
import { defineComponent } from "vue";
|
||||||
|
|
||||||
import NcContent from "@nextcloud/vue/dist/Components/NcContent";
|
import NcContent from "@nextcloud/vue/dist/Components/NcContent";
|
||||||
import NcAppContent from "@nextcloud/vue/dist/Components/NcAppContent";
|
import NcAppContent from "@nextcloud/vue/dist/Components/NcAppContent";
|
||||||
|
@ -57,37 +57,41 @@ import { getFilePickerBuilder } from "@nextcloud/dialogs";
|
||||||
import { getCurrentUser } from "@nextcloud/auth";
|
import { getCurrentUser } from "@nextcloud/auth";
|
||||||
import axios from "@nextcloud/axios";
|
import axios from "@nextcloud/axios";
|
||||||
|
|
||||||
import GlobalMixin from "../mixins/GlobalMixin";
|
|
||||||
import UserConfig from "../mixins/UserConfig";
|
|
||||||
|
|
||||||
import banner from "../assets/banner.svg";
|
import banner from "../assets/banner.svg";
|
||||||
import { IDay } from "../types";
|
import { IDay } from "../types";
|
||||||
import { API } from "../services/API";
|
import { API } from "../services/API";
|
||||||
|
|
||||||
@Component({
|
export default defineComponent({
|
||||||
|
name: "FirstStart",
|
||||||
components: {
|
components: {
|
||||||
NcContent,
|
NcContent,
|
||||||
NcAppContent,
|
NcAppContent,
|
||||||
NcButton,
|
NcButton,
|
||||||
},
|
},
|
||||||
})
|
|
||||||
export default class FirstStart extends Mixins(GlobalMixin, UserConfig) {
|
data() {
|
||||||
banner = banner;
|
return {
|
||||||
error = "";
|
banner,
|
||||||
info = "";
|
error: "",
|
||||||
show = false;
|
info: "",
|
||||||
chosenPath = "";
|
show: false,
|
||||||
|
chosenPath: "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
this.show = true;
|
this.show = true;
|
||||||
}, 300);
|
}, 300);
|
||||||
}
|
},
|
||||||
|
|
||||||
get isAdmin() {
|
computed: {
|
||||||
|
isAdmin() {
|
||||||
return getCurrentUser().isAdmin;
|
return getCurrentUser().isAdmin;
|
||||||
}
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
async begin() {
|
async begin() {
|
||||||
const path = await this.chooseFolder(
|
const path = await this.chooseFolder(
|
||||||
this.t("memories", "Choose the root of your timeline"),
|
this.t("memories", "Choose the root of your timeline"),
|
||||||
|
@ -124,14 +128,14 @@ export default class FirstStart extends Mixins(GlobalMixin, UserConfig) {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
this.chosenPath = path;
|
this.chosenPath = path;
|
||||||
}
|
},
|
||||||
|
|
||||||
async finish() {
|
async finish() {
|
||||||
this.show = false;
|
this.show = false;
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
this.config_timelinePath = this.chosenPath;
|
this.config_timelinePath = this.chosenPath;
|
||||||
await this.updateSetting("timelinePath");
|
await this.updateSetting("timelinePath");
|
||||||
}
|
},
|
||||||
|
|
||||||
async chooseFolder(title: string, initial: string) {
|
async chooseFolder(title: string, initial: string) {
|
||||||
const picker = getFilePickerBuilder(title)
|
const picker = getFilePickerBuilder(title)
|
||||||
|
@ -144,8 +148,9 @@ export default class FirstStart extends Mixins(GlobalMixin, UserConfig) {
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
return await picker.pick();
|
return await picker.pick();
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -45,8 +45,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Mixins } from "vue-property-decorator";
|
import { defineComponent } from "vue";
|
||||||
import GlobalMixin from "../mixins/GlobalMixin";
|
|
||||||
|
|
||||||
import NcActions from "@nextcloud/vue/dist/Components/NcActions";
|
import NcActions from "@nextcloud/vue/dist/Components/NcActions";
|
||||||
import NcActionButton from "@nextcloud/vue/dist/Components/NcActionButton";
|
import NcActionButton from "@nextcloud/vue/dist/Components/NcActionButton";
|
||||||
|
@ -68,53 +67,34 @@ import InfoIcon from "vue-material-design-icons/InformationOutline.vue";
|
||||||
import LocationIcon from "vue-material-design-icons/MapMarker.vue";
|
import LocationIcon from "vue-material-design-icons/MapMarker.vue";
|
||||||
import { API } from "../services/API";
|
import { API } from "../services/API";
|
||||||
|
|
||||||
@Component({
|
export default defineComponent({
|
||||||
|
name: "Metadata",
|
||||||
components: {
|
components: {
|
||||||
NcActions,
|
NcActions,
|
||||||
NcActionButton,
|
NcActionButton,
|
||||||
EditIcon,
|
EditIcon,
|
||||||
},
|
},
|
||||||
})
|
|
||||||
export default class Metadata extends Mixins(GlobalMixin) {
|
|
||||||
private fileInfo: IFileInfo = null;
|
|
||||||
private exif: { [prop: string]: any } = {};
|
|
||||||
private baseInfo: any = {};
|
|
||||||
private nominatim: any = null;
|
|
||||||
private state = 0;
|
|
||||||
|
|
||||||
public async update(fileInfo: IFileInfo) {
|
data() {
|
||||||
this.state = Math.random();
|
return {
|
||||||
this.fileInfo = fileInfo;
|
fileInfo: null as IFileInfo,
|
||||||
this.exif = {};
|
exif: {} as { [prop: string]: any },
|
||||||
this.nominatim = null;
|
baseInfo: {} as any,
|
||||||
|
nominatim: null as any,
|
||||||
const state = this.state;
|
state: 0,
|
||||||
const url = API.IMAGE_INFO(fileInfo.id);
|
};
|
||||||
const res = await axios.get<any>(url);
|
},
|
||||||
if (state !== this.state) return;
|
|
||||||
|
|
||||||
this.baseInfo = res.data;
|
|
||||||
this.exif = res.data.exif || {};
|
|
||||||
|
|
||||||
// Lazy loading
|
|
||||||
this.getNominatim().catch();
|
|
||||||
}
|
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
subscribe("files:file:updated", this.handleFileUpdated);
|
subscribe("files:file:updated", this.handleFileUpdated);
|
||||||
}
|
},
|
||||||
|
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
unsubscribe("files:file:updated", this.handleFileUpdated);
|
unsubscribe("files:file:updated", this.handleFileUpdated);
|
||||||
}
|
},
|
||||||
|
|
||||||
private handleFileUpdated({ fileid }) {
|
computed: {
|
||||||
if (fileid && this.fileInfo?.id === fileid) {
|
topFields() {
|
||||||
this.update(this.fileInfo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get topFields() {
|
|
||||||
let list: {
|
let list: {
|
||||||
title: string;
|
title: string;
|
||||||
subtitle: string[];
|
subtitle: string[];
|
||||||
|
@ -169,10 +149,10 @@ export default class Metadata extends Mixins(GlobalMixin) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Date taken info */
|
/** Date taken info */
|
||||||
get dateOriginal() {
|
dateOriginal() {
|
||||||
const dt = this.exif["DateTimeOriginal"] || this.exif["CreateDate"];
|
const dt = this.exif["DateTimeOriginal"] || this.exif["CreateDate"];
|
||||||
if (!dt) return null;
|
if (!dt) return null;
|
||||||
|
|
||||||
|
@ -180,14 +160,14 @@ export default class Metadata extends Mixins(GlobalMixin) {
|
||||||
if (!m.isValid()) return null;
|
if (!m.isValid()) return null;
|
||||||
m.locale(getCanonicalLocale());
|
m.locale(getCanonicalLocale());
|
||||||
return m;
|
return m;
|
||||||
}
|
},
|
||||||
|
|
||||||
get dateOriginalStr() {
|
dateOriginalStr() {
|
||||||
if (!this.dateOriginal) return null;
|
if (!this.dateOriginal) return null;
|
||||||
return utils.getLongDateStr(this.dateOriginal.toDate(), true);
|
return utils.getLongDateStr(this.dateOriginal.toDate(), true);
|
||||||
}
|
},
|
||||||
|
|
||||||
get dateOriginalTime() {
|
dateOriginalTime() {
|
||||||
if (!this.dateOriginal) return null;
|
if (!this.dateOriginal) return null;
|
||||||
|
|
||||||
// Try to get timezone
|
// Try to get timezone
|
||||||
|
@ -199,18 +179,18 @@ export default class Metadata extends Mixins(GlobalMixin) {
|
||||||
if (tz) parts.push(tz);
|
if (tz) parts.push(tz);
|
||||||
|
|
||||||
return parts;
|
return parts;
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Camera make and model info */
|
/** Camera make and model info */
|
||||||
get camera() {
|
camera() {
|
||||||
const make = this.exif["Make"];
|
const make = this.exif["Make"];
|
||||||
const model = this.exif["Model"];
|
const model = this.exif["Model"];
|
||||||
if (!make || !model) return null;
|
if (!make || !model) return null;
|
||||||
if (model.startsWith(make)) return model;
|
if (model.startsWith(make)) return model;
|
||||||
return `${make} ${model}`;
|
return `${make} ${model}`;
|
||||||
}
|
},
|
||||||
|
|
||||||
get cameraSub() {
|
cameraSub() {
|
||||||
const f = this.exif["FNumber"] || this.exif["Aperture"];
|
const f = this.exif["FNumber"] || this.exif["Aperture"];
|
||||||
const s = this.shutterSpeed;
|
const s = this.shutterSpeed;
|
||||||
const len = this.exif["FocalLength"];
|
const len = this.exif["FocalLength"];
|
||||||
|
@ -222,10 +202,10 @@ export default class Metadata extends Mixins(GlobalMixin) {
|
||||||
if (len) parts.push(`${len}mm`);
|
if (len) parts.push(`${len}mm`);
|
||||||
if (iso) parts.push(`ISO${iso}`);
|
if (iso) parts.push(`ISO${iso}`);
|
||||||
return parts;
|
return parts;
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Convert shutter speed decimal to 1/x format */
|
/** Convert shutter speed decimal to 1/x format */
|
||||||
get shutterSpeed() {
|
shutterSpeed() {
|
||||||
const speed = Number(
|
const speed = Number(
|
||||||
this.exif["ShutterSpeedValue"] ||
|
this.exif["ShutterSpeedValue"] ||
|
||||||
this.exif["ShutterSpeed"] ||
|
this.exif["ShutterSpeed"] ||
|
||||||
|
@ -238,14 +218,14 @@ export default class Metadata extends Mixins(GlobalMixin) {
|
||||||
} else {
|
} else {
|
||||||
return `${Math.round(speed * 10) / 10}s`;
|
return `${Math.round(speed * 10) / 10}s`;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Image info */
|
/** Image info */
|
||||||
get imageInfo() {
|
imageInfo() {
|
||||||
return this.fileInfo.basename || (<any>this.fileInfo).name;
|
return this.fileInfo.basename || (<any>this.fileInfo).name;
|
||||||
}
|
},
|
||||||
|
|
||||||
get imageInfoSub() {
|
imageInfoSub() {
|
||||||
let parts = [];
|
let parts = [];
|
||||||
let mp = Number(this.exif["Megapixels"]);
|
let mp = Number(this.exif["Megapixels"]);
|
||||||
|
|
||||||
|
@ -262,9 +242,9 @@ export default class Metadata extends Mixins(GlobalMixin) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return parts;
|
return parts;
|
||||||
}
|
},
|
||||||
|
|
||||||
get address() {
|
address() {
|
||||||
if (!this.lat || !this.lon) return null;
|
if (!this.lat || !this.lon) return null;
|
||||||
|
|
||||||
if (!this.nominatim) return this.t("memories", "Loading …");
|
if (!this.nominatim) return this.t("memories", "Loading …");
|
||||||
|
@ -281,17 +261,17 @@ export default class Metadata extends Mixins(GlobalMixin) {
|
||||||
} else {
|
} else {
|
||||||
return n.display_name;
|
return n.display_name;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
get lat() {
|
lat() {
|
||||||
return this.exif["GPSLatitude"];
|
return this.exif["GPSLatitude"];
|
||||||
}
|
},
|
||||||
|
|
||||||
get lon() {
|
lon() {
|
||||||
return this.exif["GPSLongitude"];
|
return this.exif["GPSLongitude"];
|
||||||
}
|
},
|
||||||
|
|
||||||
get mapUrl() {
|
mapUrl() {
|
||||||
const boxSize = 0.0075;
|
const boxSize = 0.0075;
|
||||||
const bbox = [
|
const bbox = [
|
||||||
this.lon - boxSize,
|
this.lon - boxSize,
|
||||||
|
@ -301,11 +281,37 @@ export default class Metadata extends Mixins(GlobalMixin) {
|
||||||
];
|
];
|
||||||
const m = `${this.lat},${this.lon}`;
|
const m = `${this.lat},${this.lon}`;
|
||||||
return `https://www.openstreetmap.org/export/embed.html?bbox=${bbox.join()}&marker=${m}`;
|
return `https://www.openstreetmap.org/export/embed.html?bbox=${bbox.join()}&marker=${m}`;
|
||||||
}
|
},
|
||||||
|
|
||||||
get mapFullUrl() {
|
mapFullUrl() {
|
||||||
return `https://www.openstreetmap.org/?mlat=${this.lat}&mlon=${this.lon}#map=18/${this.lat}/${this.lon}`;
|
return `https://www.openstreetmap.org/?mlat=${this.lat}&mlon=${this.lon}#map=18/${this.lat}/${this.lon}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async update(fileInfo: IFileInfo) {
|
||||||
|
this.state = Math.random();
|
||||||
|
this.fileInfo = fileInfo;
|
||||||
|
this.exif = {};
|
||||||
|
this.nominatim = null;
|
||||||
|
|
||||||
|
const state = this.state;
|
||||||
|
const url = API.IMAGE_INFO(fileInfo.id);
|
||||||
|
const res = await axios.get<any>(url);
|
||||||
|
if (state !== this.state) return;
|
||||||
|
|
||||||
|
this.baseInfo = res.data;
|
||||||
|
this.exif = res.data.exif || {};
|
||||||
|
|
||||||
|
// Lazy loading
|
||||||
|
this.getNominatim().catch();
|
||||||
|
},
|
||||||
|
|
||||||
|
handleFileUpdated({ fileid }) {
|
||||||
|
if (fileid && this.fileInfo?.id === fileid) {
|
||||||
|
this.update(this.fileInfo);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async getNominatim() {
|
async getNominatim() {
|
||||||
const lat = this.lat;
|
const lat = this.lat;
|
||||||
|
@ -318,8 +324,9 @@ export default class Metadata extends Mixins(GlobalMixin) {
|
||||||
);
|
);
|
||||||
if (state !== this.state) return;
|
if (state !== this.state) return;
|
||||||
this.nominatim = n.data;
|
this.nominatim = n.data;
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -49,9 +49,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Mixins, Prop } from "vue-property-decorator";
|
import { defineComponent, PropType } from "vue";
|
||||||
import { IRow, IRowType, ITick } from "../types";
|
import { IRow, IRowType, ITick } from "../types";
|
||||||
import GlobalMixin from "../mixins/GlobalMixin";
|
|
||||||
import ScrollIcon from "vue-material-design-icons/UnfoldMoreHorizontal.vue";
|
import ScrollIcon from "vue-material-design-icons/UnfoldMoreHorizontal.vue";
|
||||||
|
|
||||||
import * as utils from "../services/Utils";
|
import * as utils from "../services/Utils";
|
||||||
|
@ -59,56 +58,63 @@ import * as utils from "../services/Utils";
|
||||||
// Pixels to snap at
|
// Pixels to snap at
|
||||||
const SNAP_OFFSET = -35;
|
const SNAP_OFFSET = -35;
|
||||||
|
|
||||||
@Component({
|
export default defineComponent({
|
||||||
|
name: "ScrollerManager",
|
||||||
components: {
|
components: {
|
||||||
ScrollIcon,
|
ScrollIcon,
|
||||||
},
|
},
|
||||||
})
|
|
||||||
export default class ScrollerManager extends Mixins(GlobalMixin) {
|
props: {
|
||||||
/** Rows from Timeline */
|
/** Rows from Timeline */
|
||||||
@Prop() rows!: IRow[];
|
rows: Array as PropType<IRow[]>,
|
||||||
/** Total height */
|
/** Total height */
|
||||||
@Prop() height!: number;
|
height: Number,
|
||||||
/** Actual recycler component */
|
/** Actual recycler component */
|
||||||
@Prop() recycler!: any;
|
recycler: Object,
|
||||||
/** Recycler before slot component */
|
/** Recycler before slot component */
|
||||||
@Prop() recyclerBefore!: any;
|
recyclerBefore: HTMLDivElement,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
/** Last known height at adjustment */
|
/** Last known height at adjustment */
|
||||||
private lastAdjustHeight = 0;
|
lastAdjustHeight: 0,
|
||||||
/** Height of the entire photo view */
|
/** Height of the entire photo view */
|
||||||
private recyclerHeight: number = 100;
|
recyclerHeight: 100,
|
||||||
/** Rect of scroller */
|
/** Rect of scroller */
|
||||||
private scrollerRect: DOMRect = null;
|
scrollerRect: null as DOMRect,
|
||||||
/** Computed ticks */
|
/** Computed ticks */
|
||||||
private ticks: ITick[] = [];
|
ticks: [] as ITick[],
|
||||||
/** Computed cursor top */
|
/** Computed cursor top */
|
||||||
private cursorY = 0;
|
cursorY: 0,
|
||||||
/** Hover cursor top */
|
/** Hover cursor top */
|
||||||
private hoverCursorY = -5;
|
hoverCursorY: -5,
|
||||||
/** Hover cursor text */
|
/** Hover cursor text */
|
||||||
private hoverCursorText = "";
|
hoverCursorText: "",
|
||||||
/** Scrolling using the scroller */
|
/** Scrolling using the scroller */
|
||||||
private scrollingTimer = 0;
|
scrollingTimer: 0,
|
||||||
/** Scrolling now using the scroller */
|
/** Scrolling now using the scroller */
|
||||||
private scrollingNowTimer = 0;
|
scrollingNowTimer: 0,
|
||||||
/** Scrolling recycler */
|
/** Scrolling recycler */
|
||||||
private scrollingRecyclerTimer = 0;
|
scrollingRecyclerTimer: 0,
|
||||||
/** Scrolling recycler now */
|
/** Scrolling recycler now */
|
||||||
private scrollingRecyclerNowTimer = 0;
|
scrollingRecyclerNowTimer: 0,
|
||||||
/** Recycler scrolling throttle */
|
/** Recycler scrolling throttle */
|
||||||
private scrollingRecyclerUpdateTimer = 0;
|
scrollingRecyclerUpdateTimer: 0,
|
||||||
/** View size reflow timer */
|
/** View size reflow timer */
|
||||||
private reflowRequest = false;
|
reflowRequest: false,
|
||||||
/** Tick adjust timer */
|
/** Tick adjust timer */
|
||||||
private adjustRequest = false;
|
adjustRequest: false,
|
||||||
/** Scroller is being moved with interaction */
|
/** Scroller is being moved with interaction */
|
||||||
private interacting = false;
|
interacting: false,
|
||||||
/** Track the last requested y position when interacting */
|
/** Track the last requested y position when interacting */
|
||||||
private lastRequestedRecyclerY = 0;
|
lastRequestedRecyclerY: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
/** Get the visible ticks */
|
/** Get the visible ticks */
|
||||||
get visibleTicks() {
|
visibleTicks() {
|
||||||
let key = 9999999900;
|
let key = 9999999900;
|
||||||
return this.ticks
|
return this.ticks
|
||||||
.filter((tick) => tick.s)
|
.filter((tick) => tick.s)
|
||||||
|
@ -120,10 +126,12 @@ export default class ScrollerManager extends Mixins(GlobalMixin) {
|
||||||
}
|
}
|
||||||
return tick;
|
return tick;
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
/** Reset state */
|
/** Reset state */
|
||||||
public reset() {
|
reset() {
|
||||||
this.ticks = [];
|
this.ticks = [];
|
||||||
this.cursorY = 0;
|
this.cursorY = 0;
|
||||||
this.hoverCursorY = -5;
|
this.hoverCursorY = -5;
|
||||||
|
@ -141,10 +149,10 @@ export default class ScrollerManager extends Mixins(GlobalMixin) {
|
||||||
this.scrollingRecyclerTimer = 0;
|
this.scrollingRecyclerTimer = 0;
|
||||||
this.scrollingRecyclerNowTimer = 0;
|
this.scrollingRecyclerNowTimer = 0;
|
||||||
this.scrollingRecyclerUpdateTimer = 0;
|
this.scrollingRecyclerUpdateTimer = 0;
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Recycler scroll event, must be called by timeline */
|
/** Recycler scroll event, must be called by timeline */
|
||||||
public recyclerScrolled() {
|
recyclerScrolled() {
|
||||||
// This isn't a renewing timer, it's a scheduled task
|
// This isn't a renewing timer, it's a scheduled task
|
||||||
if (this.scrollingRecyclerUpdateTimer) return;
|
if (this.scrollingRecyclerUpdateTimer) return;
|
||||||
this.scrollingRecyclerUpdateTimer = window.setTimeout(() => {
|
this.scrollingRecyclerUpdateTimer = window.setTimeout(() => {
|
||||||
|
@ -155,10 +163,10 @@ export default class ScrollerManager extends Mixins(GlobalMixin) {
|
||||||
// Update that we're scrolling with the recycler
|
// Update that we're scrolling with the recycler
|
||||||
utils.setRenewingTimeout(this, "scrollingRecyclerNowTimer", null, 200);
|
utils.setRenewingTimeout(this, "scrollingRecyclerNowTimer", null, 200);
|
||||||
utils.setRenewingTimeout(this, "scrollingRecyclerTimer", null, 1500);
|
utils.setRenewingTimeout(this, "scrollingRecyclerTimer", null, 1500);
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Update cursor position from recycler scroll position */
|
/** Update cursor position from recycler scroll position */
|
||||||
public updateFromRecyclerScroll() {
|
updateFromRecyclerScroll() {
|
||||||
// Ignore if not initialized or moving
|
// Ignore if not initialized or moving
|
||||||
if (!this.ticks.length || this.interacting) return;
|
if (!this.ticks.length || this.interacting) return;
|
||||||
|
|
||||||
|
@ -180,19 +188,19 @@ export default class ScrollerManager extends Mixins(GlobalMixin) {
|
||||||
} else {
|
} else {
|
||||||
this.moveHoverCursor(rtop);
|
this.moveHoverCursor(rtop);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Re-create tick data in the next frame */
|
/** Re-create tick data in the next frame */
|
||||||
public async reflow() {
|
async reflow() {
|
||||||
if (this.reflowRequest) return;
|
if (this.reflowRequest) return;
|
||||||
this.reflowRequest = true;
|
this.reflowRequest = true;
|
||||||
await this.$nextTick();
|
await this.$nextTick();
|
||||||
this.reflowNow();
|
this.reflowNow();
|
||||||
this.reflowRequest = false;
|
this.reflowRequest = false;
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Re-create tick data */
|
/** Re-create tick data */
|
||||||
private reflowNow() {
|
reflowNow() {
|
||||||
// Ignore if not initialized
|
// Ignore if not initialized
|
||||||
if (!this.recycler?.$refs.wrapper) return;
|
if (!this.recycler?.$refs.wrapper) return;
|
||||||
|
|
||||||
|
@ -204,10 +212,10 @@ export default class ScrollerManager extends Mixins(GlobalMixin) {
|
||||||
|
|
||||||
// Adjust top
|
// Adjust top
|
||||||
this.adjustNow();
|
this.adjustNow();
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Recreate from scratch */
|
/** Recreate from scratch */
|
||||||
private recreate() {
|
recreate() {
|
||||||
// Clear and override any adjust timer
|
// Clear and override any adjust timer
|
||||||
this.ticks = [];
|
this.ticks = [];
|
||||||
|
|
||||||
|
@ -256,22 +264,22 @@ export default class ScrollerManager extends Mixins(GlobalMixin) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update tick positions without truncating the list
|
* Update tick positions without truncating the list
|
||||||
* This is much cheaper than reflowing the whole thing
|
* This is much cheaper than reflowing the whole thing
|
||||||
*/
|
*/
|
||||||
public async adjust() {
|
async adjust() {
|
||||||
if (this.adjustRequest) return;
|
if (this.adjustRequest) return;
|
||||||
this.adjustRequest = true;
|
this.adjustRequest = true;
|
||||||
await this.$nextTick();
|
await this.$nextTick();
|
||||||
this.adjustNow();
|
this.adjustNow();
|
||||||
this.adjustRequest = false;
|
this.adjustRequest = false;
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Do adjustment synchronously */
|
/** Do adjustment synchronously */
|
||||||
private adjustNow() {
|
adjustNow() {
|
||||||
// Refresh height of recycler
|
// Refresh height of recycler
|
||||||
this.recyclerHeight = this.recycler.$refs.wrapper.clientHeight;
|
this.recyclerHeight = this.recycler.$refs.wrapper.clientHeight;
|
||||||
const extraY = this.recyclerBefore?.clientHeight || 0;
|
const extraY = this.recyclerBefore?.clientHeight || 0;
|
||||||
|
@ -319,10 +327,10 @@ export default class ScrollerManager extends Mixins(GlobalMixin) {
|
||||||
this.setTicksTop(count);
|
this.setTicksTop(count);
|
||||||
this.computeVisibleTicks();
|
this.computeVisibleTicks();
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Mark ticks as visible or invisible */
|
/** Mark ticks as visible or invisible */
|
||||||
private computeVisibleTicks() {
|
computeVisibleTicks() {
|
||||||
// Kind of unrelated here, but refresh rect
|
// Kind of unrelated here, but refresh rect
|
||||||
this.scrollerRect = (
|
this.scrollerRect = (
|
||||||
this.$refs.scroller as HTMLElement
|
this.$refs.scroller as HTMLElement
|
||||||
|
@ -381,17 +389,17 @@ export default class ScrollerManager extends Mixins(GlobalMixin) {
|
||||||
tick.s = true;
|
tick.s = true;
|
||||||
prevShow = tick.top;
|
prevShow = tick.top;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
private setTicksTop(total: number) {
|
setTicksTop(total: number) {
|
||||||
for (const tick of this.ticks) {
|
for (const tick of this.ticks) {
|
||||||
tick.topF = this.height * (tick.count / total);
|
tick.topF = this.height * (tick.count / total);
|
||||||
tick.top = utils.roundHalf(tick.topF);
|
tick.top = utils.roundHalf(tick.topF);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Change actual position of the hover cursor */
|
/** Change actual position of the hover cursor */
|
||||||
private moveHoverCursor(y: number) {
|
moveHoverCursor(y: number) {
|
||||||
this.hoverCursorY = y;
|
this.hoverCursorY = y;
|
||||||
|
|
||||||
// Get index of previous tick
|
// Get index of previous tick
|
||||||
|
@ -415,24 +423,24 @@ export default class ScrollerManager extends Mixins(GlobalMixin) {
|
||||||
|
|
||||||
const date = utils.dayIdToDate(dayId);
|
const date = utils.dayIdToDate(dayId);
|
||||||
this.hoverCursorText = utils.getShortDateStr(date);
|
this.hoverCursorText = utils.getShortDateStr(date);
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Handle mouse hover */
|
/** Handle mouse hover */
|
||||||
private mousemove(event: MouseEvent) {
|
mousemove(event: MouseEvent) {
|
||||||
if (event.buttons) {
|
if (event.buttons) {
|
||||||
this.mousedown(event);
|
this.mousedown(event);
|
||||||
}
|
}
|
||||||
this.moveHoverCursor(event.offsetY);
|
this.moveHoverCursor(event.offsetY);
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Handle mouse leave */
|
/** Handle mouse leave */
|
||||||
private mouseleave() {
|
mouseleave() {
|
||||||
this.interactend();
|
this.interactend();
|
||||||
this.moveHoverCursor(this.cursorY);
|
this.moveHoverCursor(this.cursorY);
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Binary search and get coords surrounding position */
|
/** Binary search and get coords surrounding position */
|
||||||
private getCoords(y: number, field: "topF" | "y") {
|
getCoords(y: number, field: "topF" | "y") {
|
||||||
// Top of first and second ticks
|
// Top of first and second ticks
|
||||||
let top1 = 0,
|
let top1 = 0,
|
||||||
top2 = 0,
|
top2 = 0,
|
||||||
|
@ -462,10 +470,10 @@ export default class ScrollerManager extends Mixins(GlobalMixin) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return { top1, top2, y1, y2 };
|
return { top1, top2, y1, y2 };
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Move to given scroller Y */
|
/** Move to given scroller Y */
|
||||||
private moveto(y: number, snap: boolean) {
|
moveto(y: number, snap: boolean) {
|
||||||
// Move cursor immediately to prevent jank
|
// Move cursor immediately to prevent jank
|
||||||
this.cursorY = y;
|
this.cursorY = y;
|
||||||
this.hoverCursorY = y;
|
this.hoverCursorY = y;
|
||||||
|
@ -481,36 +489,37 @@ export default class ScrollerManager extends Mixins(GlobalMixin) {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.handleScroll();
|
this.handleScroll();
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Handle mouse click */
|
/** Handle mouse click */
|
||||||
private mousedown(event: MouseEvent) {
|
mousedown(event: MouseEvent) {
|
||||||
this.interactstart(); // end called on mouseup
|
this.interactstart(); // end called on mouseup
|
||||||
this.moveto(event.offsetY, false);
|
this.moveto(event.offsetY, false);
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Handle touch */
|
/** Handle touch */
|
||||||
private touchmove(event: any) {
|
touchmove(event: any) {
|
||||||
let y = event.targetTouches[0].pageY - this.scrollerRect.top;
|
let y = event.targetTouches[0].pageY - this.scrollerRect.top;
|
||||||
y = Math.max(0, y - 20); // middle of touch finger
|
y = Math.max(0, y - 20); // middle of touch finger
|
||||||
this.moveto(y, true);
|
this.moveto(y, true);
|
||||||
}
|
},
|
||||||
|
|
||||||
private interactstart() {
|
interactstart() {
|
||||||
this.interacting = true;
|
this.interacting = true;
|
||||||
}
|
},
|
||||||
|
|
||||||
private interactend() {
|
interactend() {
|
||||||
this.interacting = false;
|
this.interacting = false;
|
||||||
this.recyclerScrolled(); // make sure final position is correct
|
this.recyclerScrolled(); // make sure final position is correct
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Update scroller is being used to scroll recycler */
|
/** Update scroller is being used to scroll recycler */
|
||||||
private handleScroll() {
|
handleScroll() {
|
||||||
utils.setRenewingTimeout(this, "scrollingNowTimer", null, 200);
|
utils.setRenewingTimeout(this, "scrollingNowTimer", null, 200);
|
||||||
utils.setRenewingTimeout(this, "scrollingTimer", null, 1500);
|
utils.setRenewingTimeout(this, "scrollingTimer", null, 1500);
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -48,9 +48,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Emit, Mixins, Prop, Watch } from "vue-property-decorator";
|
import { defineComponent, PropType } from "vue";
|
||||||
import GlobalMixin from "../mixins/GlobalMixin";
|
|
||||||
import UserConfig from "../mixins/UserConfig";
|
|
||||||
|
|
||||||
import { showError } from "@nextcloud/dialogs";
|
import { showError } from "@nextcloud/dialogs";
|
||||||
|
|
||||||
|
@ -91,7 +89,8 @@ import AlbumRemoveIcon from "vue-material-design-icons/BookRemove.vue";
|
||||||
|
|
||||||
type Selection = Map<number, IPhoto>;
|
type Selection = Map<number, IPhoto>;
|
||||||
|
|
||||||
@Component({
|
export default defineComponent({
|
||||||
|
name: "SelectionManager",
|
||||||
components: {
|
components: {
|
||||||
NcActions,
|
NcActions,
|
||||||
NcActionButton,
|
NcActionButton,
|
||||||
|
@ -102,46 +101,35 @@ type Selection = Map<number, IPhoto>;
|
||||||
|
|
||||||
CloseIcon,
|
CloseIcon,
|
||||||
},
|
},
|
||||||
})
|
|
||||||
export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
|
|
||||||
@Prop() public heads: { [dayid: number]: IHeadRow };
|
|
||||||
|
|
||||||
|
props: {
|
||||||
|
heads: Object as PropType<{ [dayid: number]: IHeadRow }>,
|
||||||
/** List of rows for multi selection */
|
/** List of rows for multi selection */
|
||||||
@Prop() public rows: IRow[];
|
rows: Array as PropType<IRow[]>,
|
||||||
|
|
||||||
/** Rows are in ascending order (desc is normal) */
|
/** Rows are in ascending order (desc is normal) */
|
||||||
@Prop() public isreverse: boolean;
|
isreverse: Boolean,
|
||||||
|
|
||||||
/** Recycler element to scroll during touch multi-select */
|
/** Recycler element to scroll during touch multi-select */
|
||||||
@Prop() public recycler: any;
|
recycler: Object,
|
||||||
|
},
|
||||||
|
|
||||||
private show = false;
|
data() {
|
||||||
private size = 0;
|
return {
|
||||||
private readonly selection!: Selection;
|
show: false,
|
||||||
private readonly defaultActions: ISelectionAction[];
|
size: 0,
|
||||||
|
selection: new Map<number, IPhoto>(),
|
||||||
|
defaultActions: null as ISelectionAction[],
|
||||||
|
|
||||||
private touchAnchor: IPhoto = null;
|
touchAnchor: null as IPhoto,
|
||||||
private touchTimer: number = 0;
|
touchTimer: 0,
|
||||||
private touchPrevSel!: Selection;
|
touchPrevSel: null as Selection,
|
||||||
private prevOver!: IPhoto;
|
prevOver: null as IPhoto,
|
||||||
private touchScrollInterval: number = 0;
|
touchScrollInterval: 0,
|
||||||
private touchScrollDelta: number = 0;
|
touchScrollDelta: 0,
|
||||||
private prevTouch: Touch = null;
|
prevTouch: null as Touch,
|
||||||
|
};
|
||||||
@Emit("refresh")
|
},
|
||||||
refresh() {}
|
|
||||||
|
|
||||||
@Emit("delete")
|
|
||||||
deletePhotos(photos: IPhoto[]) {}
|
|
||||||
|
|
||||||
@Emit("updateLoading")
|
|
||||||
updateLoading(delta: number) {}
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.selection = new Map<number, IPhoto>();
|
|
||||||
|
|
||||||
|
mounted() {
|
||||||
// Make default actions
|
// Make default actions
|
||||||
this.defaultActions = [
|
this.defaultActions = [
|
||||||
{
|
{
|
||||||
|
@ -230,66 +218,83 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
|
||||||
this.editDateSelection(getSel(photo));
|
this.editDateSelection(getSel(photo));
|
||||||
globalThis.editExif = (photo: IPhoto) =>
|
globalThis.editExif = (photo: IPhoto) =>
|
||||||
this.editExifSelection(getSel(photo));
|
this.editExifSelection(getSel(photo));
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Download is not allowed on some public shares */
|
watch: {
|
||||||
private allowDownload(): boolean {
|
show() {
|
||||||
return this.state_noDownload;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Archive is not allowed only on folder routes */
|
|
||||||
private allowArchive() {
|
|
||||||
return this.$route.name !== "folders";
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Is archive route */
|
|
||||||
private routeIsArchive() {
|
|
||||||
return this.$route.name === "archive";
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Is album route */
|
|
||||||
private routeIsAlbum() {
|
|
||||||
return this.config_albumsEnabled && this.$route.name === "albums";
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Public route that can't modify anything */
|
|
||||||
private routeIsPublic() {
|
|
||||||
return this.$route.name === "folder-share";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Watch("show")
|
|
||||||
onShowChange() {
|
|
||||||
const klass = "has-top-bar";
|
const klass = "has-top-bar";
|
||||||
if (this.show) {
|
if (this.show) {
|
||||||
document.body.classList.add(klass);
|
document.body.classList.add(klass);
|
||||||
} else {
|
} else {
|
||||||
document.body.classList.remove(klass);
|
document.body.classList.remove(klass);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
refresh() {
|
||||||
|
this.$emit("refresh");
|
||||||
|
},
|
||||||
|
|
||||||
|
deletePhotos(photos: IPhoto[]) {
|
||||||
|
this.$emit("deletePhotos", photos);
|
||||||
|
},
|
||||||
|
|
||||||
|
updateLoading(delta: number) {
|
||||||
|
this.$emit("updateLoading", delta);
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Download is not allowed on some public shares */
|
||||||
|
allowDownload(): boolean {
|
||||||
|
return this.state_noDownload;
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Archive is not allowed only on folder routes */
|
||||||
|
allowArchive() {
|
||||||
|
return this.$route.name !== "folders";
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Is archive route */
|
||||||
|
routeIsArchive() {
|
||||||
|
return this.$route.name === "archive";
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Is album route */
|
||||||
|
routeIsAlbum() {
|
||||||
|
return this.config_albumsEnabled && this.$route.name === "albums";
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Public route that can't modify anything */
|
||||||
|
routeIsPublic() {
|
||||||
|
return this.$route.name === "folder-share";
|
||||||
|
},
|
||||||
|
|
||||||
/** Trigger to update props from selection set */
|
/** Trigger to update props from selection set */
|
||||||
private selectionChanged() {
|
selectionChanged() {
|
||||||
this.show = this.selection.size > 0;
|
this.show = this.selection.size > 0;
|
||||||
this.size = this.selection.size;
|
this.size = this.selection.size;
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Is this fileid (or anything if not specified) selected */
|
/** Is this fileid (or anything if not specified) selected */
|
||||||
public has(fileid?: number) {
|
has(fileid?: number) {
|
||||||
if (fileid === undefined) {
|
if (fileid === undefined) {
|
||||||
return this.selection.size > 0;
|
return this.selection.size > 0;
|
||||||
}
|
}
|
||||||
return this.selection.has(fileid);
|
return this.selection.has(fileid);
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Get the actions list */
|
/** Get the actions list */
|
||||||
private getActions(): ISelectionAction[] {
|
getActions(): ISelectionAction[] {
|
||||||
return this.defaultActions.filter(
|
return (
|
||||||
(a) => (!a.if || a.if(this)) && (!this.routeIsPublic() || a.allowPublic)
|
this.defaultActions?.filter(
|
||||||
|
(a) =>
|
||||||
|
(!a.if || a.if(this)) && (!this.routeIsPublic() || a.allowPublic)
|
||||||
|
) || []
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Click on an action */
|
/** Click on an action */
|
||||||
private async click(action: ISelectionAction) {
|
async click(action: ISelectionAction) {
|
||||||
try {
|
try {
|
||||||
this.updateLoading(1);
|
this.updateLoading(1);
|
||||||
await action.callback(this.selection);
|
await action.callback(this.selection);
|
||||||
|
@ -298,10 +303,10 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
|
||||||
} finally {
|
} finally {
|
||||||
this.updateLoading(-1);
|
this.updateLoading(-1);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Clicking on photo */
|
/** Clicking on photo */
|
||||||
public clickPhoto(photo: IPhoto, event: PointerEvent, rowIdx: number) {
|
clickPhoto(photo: IPhoto, event: PointerEvent, rowIdx: number) {
|
||||||
if (photo.flag & this.c.FLAG_PLACEHOLDER) return;
|
if (photo.flag & this.c.FLAG_PLACEHOLDER) return;
|
||||||
if (event.pointerType === "touch") return; // let touch events handle this
|
if (event.pointerType === "touch") return; // let touch events handle this
|
||||||
|
|
||||||
|
@ -314,10 +319,10 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
|
||||||
} else {
|
} else {
|
||||||
this.openViewer(photo);
|
this.openViewer(photo);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Tap on */
|
/** Tap on */
|
||||||
protected touchstartPhoto(photo: IPhoto, event: TouchEvent, rowIdx: number) {
|
touchstartPhoto(photo: IPhoto, event: TouchEvent, rowIdx: number) {
|
||||||
if (photo.flag & this.c.FLAG_PLACEHOLDER) return;
|
if (photo.flag & this.c.FLAG_PLACEHOLDER) return;
|
||||||
this.rows[rowIdx].virtualSticky = true;
|
this.rows[rowIdx].virtualSticky = true;
|
||||||
|
|
||||||
|
@ -332,18 +337,18 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
|
||||||
}
|
}
|
||||||
this.touchTimer = 0;
|
this.touchTimer = 0;
|
||||||
}, 600);
|
}, 600);
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Tap off */
|
/** Tap off */
|
||||||
protected touchendPhoto(photo: IPhoto, event: TouchEvent, rowIdx: number) {
|
touchendPhoto(photo: IPhoto, event: TouchEvent, rowIdx: number) {
|
||||||
if (photo.flag & this.c.FLAG_PLACEHOLDER) return;
|
if (photo.flag & this.c.FLAG_PLACEHOLDER) return;
|
||||||
delete this.rows[rowIdx].virtualSticky;
|
delete this.rows[rowIdx].virtualSticky;
|
||||||
|
|
||||||
if (this.touchTimer) this.clickPhoto(photo, {} as any, rowIdx);
|
if (this.touchTimer) this.clickPhoto(photo, {} as any, rowIdx);
|
||||||
this.resetTouchParams();
|
this.resetTouchParams();
|
||||||
}
|
},
|
||||||
|
|
||||||
private resetTouchParams() {
|
resetTouchParams() {
|
||||||
window.clearTimeout(this.touchTimer);
|
window.clearTimeout(this.touchTimer);
|
||||||
this.touchTimer = 0;
|
this.touchTimer = 0;
|
||||||
this.touchAnchor = null;
|
this.touchAnchor = null;
|
||||||
|
@ -353,13 +358,13 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
|
||||||
this.touchScrollInterval = 0;
|
this.touchScrollInterval = 0;
|
||||||
|
|
||||||
this.prevTouch = null;
|
this.prevTouch = null;
|
||||||
}
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tap over
|
* Tap over
|
||||||
* photo and rowIdx are that of the *anchor*
|
* photo and rowIdx are that of the *anchor*
|
||||||
*/
|
*/
|
||||||
protected touchmovePhoto(anchor: IPhoto, event: TouchEvent, rowIdx: number) {
|
touchmovePhoto(anchor: IPhoto, event: TouchEvent, rowIdx: number) {
|
||||||
if (anchor.flag & this.c.FLAG_PLACEHOLDER) return;
|
if (anchor.flag & this.c.FLAG_PLACEHOLDER) return;
|
||||||
|
|
||||||
if (this.touchTimer) {
|
if (this.touchTimer) {
|
||||||
|
@ -415,15 +420,16 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.touchMoveSelect(touch, rowIdx);
|
this.touchMoveSelect(touch, rowIdx);
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Multi-select triggered by touchmove */
|
/** Multi-select triggered by touchmove */
|
||||||
private touchMoveSelect(touch: Touch, rowIdx: number) {
|
touchMoveSelect(touch: Touch, rowIdx: number) {
|
||||||
// Which photo is the cursor over, if any
|
// Which photo is the cursor over, if any
|
||||||
const elems = document.elementsFromPoint(touch.clientX, touch.clientY);
|
const elems = document.elementsFromPoint(touch.clientX, touch.clientY);
|
||||||
const photoComp: any = elems.find((e) => e.classList.contains("p-outer"));
|
const photoComp: any = elems.find((e) => e.classList.contains("p-outer"));
|
||||||
let overPhoto: IPhoto = photoComp?.__vue__?.data;
|
let overPhoto: IPhoto = photoComp?.__vue__?.data;
|
||||||
if (overPhoto && overPhoto.flag & this.c.FLAG_PLACEHOLDER) overPhoto = null;
|
if (overPhoto && overPhoto.flag & this.c.FLAG_PLACEHOLDER)
|
||||||
|
overPhoto = null;
|
||||||
|
|
||||||
// Do multi-selection "till" overPhoto "from" anchor
|
// Do multi-selection "till" overPhoto "from" anchor
|
||||||
// This logic is completely different from the desktop because of the
|
// This logic is completely different from the desktop because of the
|
||||||
|
@ -498,10 +504,10 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
|
||||||
|
|
||||||
this.$forceUpdate();
|
this.$forceUpdate();
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Add a photo to selection list */
|
/** Add a photo to selection list */
|
||||||
public selectPhoto(photo: IPhoto, val?: boolean, noUpdate?: boolean) {
|
selectPhoto(photo: IPhoto, val?: boolean, noUpdate?: boolean) {
|
||||||
if (
|
if (
|
||||||
photo.flag & this.c.FLAG_PLACEHOLDER ||
|
photo.flag & this.c.FLAG_PLACEHOLDER ||
|
||||||
photo.flag & this.c.FLAG_IS_FOLDER ||
|
photo.flag & this.c.FLAG_IS_FOLDER ||
|
||||||
|
@ -531,10 +537,10 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
|
||||||
this.updateHeadSelected(this.heads[photo.d.dayid]);
|
this.updateHeadSelected(this.heads[photo.d.dayid]);
|
||||||
this.$forceUpdate();
|
this.$forceUpdate();
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Multi-select */
|
/** Multi-select */
|
||||||
public selectMulti(photo: IPhoto, rows: IRow[], rowIdx: number) {
|
selectMulti(photo: IPhoto, rows: IRow[], rowIdx: number) {
|
||||||
const pRow = rows[rowIdx];
|
const pRow = rows[rowIdx];
|
||||||
const pIdx = pRow.photos.indexOf(photo);
|
const pIdx = pRow.photos.indexOf(photo);
|
||||||
if (pIdx === -1) return;
|
if (pIdx === -1) return;
|
||||||
|
@ -575,7 +581,7 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
|
||||||
|
|
||||||
// Clear everything else in front
|
// Clear everything else in front
|
||||||
Array.from(this.selection.values())
|
Array.from(this.selection.values())
|
||||||
.filter((p) => {
|
.filter((p: IPhoto) => {
|
||||||
return this.isreverse
|
return this.isreverse
|
||||||
? p.d.dayid > photo.d.dayid
|
? p.d.dayid > photo.d.dayid
|
||||||
: p.d.dayid < photo.d.dayid;
|
: p.d.dayid < photo.d.dayid;
|
||||||
|
@ -589,10 +595,10 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
|
||||||
updateDaySet.forEach((d) => this.updateHeadSelected(this.heads[d]));
|
updateDaySet.forEach((d) => this.updateHeadSelected(this.heads[d]));
|
||||||
this.$forceUpdate();
|
this.$forceUpdate();
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Select or deselect all photos in a head */
|
/** Select or deselect all photos in a head */
|
||||||
public selectHead(head: IHeadRow) {
|
selectHead(head: IHeadRow) {
|
||||||
head.selected = !head.selected;
|
head.selected = !head.selected;
|
||||||
for (const row of head.day.rows) {
|
for (const row of head.day.rows) {
|
||||||
for (const photo of row.photos) {
|
for (const photo of row.photos) {
|
||||||
|
@ -600,10 +606,10 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.$forceUpdate();
|
this.$forceUpdate();
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Check if the day for a photo is selected entirely */
|
/** Check if the day for a photo is selected entirely */
|
||||||
private updateHeadSelected(head: IHeadRow) {
|
updateHeadSelected(head: IHeadRow) {
|
||||||
let selected = true;
|
let selected = true;
|
||||||
|
|
||||||
// Check if all photos are selected
|
// Check if all photos are selected
|
||||||
|
@ -618,10 +624,10 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
|
||||||
|
|
||||||
// Update head
|
// Update head
|
||||||
head.selected = selected;
|
head.selected = selected;
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Clear all selected photos */
|
/** Clear all selected photos */
|
||||||
public clearSelection(only?: IPhoto[]) {
|
clearSelection(only?: IPhoto[]) {
|
||||||
const heads = new Set<IHeadRow>();
|
const heads = new Set<IHeadRow>();
|
||||||
const toClear = only || this.selection.values();
|
const toClear = only || this.selection.values();
|
||||||
Array.from(toClear).forEach((photo: IPhoto) => {
|
Array.from(toClear).forEach((photo: IPhoto) => {
|
||||||
|
@ -632,10 +638,10 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
|
||||||
});
|
});
|
||||||
heads.forEach(this.updateHeadSelected);
|
heads.forEach(this.updateHeadSelected);
|
||||||
this.$forceUpdate();
|
this.$forceUpdate();
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Restore selections from new day object */
|
/** Restore selections from new day object */
|
||||||
public restoreDay(day: IDay) {
|
restoreDay(day: IDay) {
|
||||||
if (!this.has()) {
|
if (!this.has()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -665,12 +671,12 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.selectionChanged();
|
this.selectionChanged();
|
||||||
}
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download the currently selected files
|
* Download the currently selected files
|
||||||
*/
|
*/
|
||||||
private async downloadSelection(selection: Selection) {
|
async downloadSelection(selection: Selection) {
|
||||||
if (selection.size >= 100) {
|
if (selection.size >= 100) {
|
||||||
if (
|
if (
|
||||||
!confirm(
|
!confirm(
|
||||||
|
@ -684,21 +690,21 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await dav.downloadFilesByPhotos(Array.from(selection.values()));
|
await dav.downloadFilesByPhotos(Array.from(selection.values()));
|
||||||
}
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if all files selected currently are favorites
|
* Check if all files selected currently are favorites
|
||||||
*/
|
*/
|
||||||
private allSelectedFavorites(selection: Selection) {
|
allSelectedFavorites(selection: Selection) {
|
||||||
return Array.from(selection.values()).every(
|
return Array.from(selection.values()).every(
|
||||||
(p) => p.flag & this.c.FLAG_IS_FAVORITE
|
(p) => p.flag & this.c.FLAG_IS_FAVORITE
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Favorite the currently selected photos
|
* Favorite the currently selected photos
|
||||||
*/
|
*/
|
||||||
private async favoriteSelection(selection: Selection) {
|
async favoriteSelection(selection: Selection) {
|
||||||
const val = !this.allSelectedFavorites(selection);
|
const val = !this.allSelectedFavorites(selection);
|
||||||
for await (const favIds of dav.favoritePhotos(
|
for await (const favIds of dav.favoritePhotos(
|
||||||
Array.from(selection.values()),
|
Array.from(selection.values()),
|
||||||
|
@ -706,12 +712,12 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
|
||||||
)) {
|
)) {
|
||||||
}
|
}
|
||||||
this.clearSelection();
|
this.clearSelection();
|
||||||
}
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete the currently selected photos
|
* Delete the currently selected photos
|
||||||
*/
|
*/
|
||||||
private async deleteSelection(selection: Selection) {
|
async deleteSelection(selection: Selection) {
|
||||||
if (selection.size >= 100) {
|
if (selection.size >= 100) {
|
||||||
if (
|
if (
|
||||||
!confirm(
|
!confirm(
|
||||||
|
@ -733,36 +739,36 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
|
||||||
.map((id) => selection.get(id));
|
.map((id) => selection.get(id));
|
||||||
this.deletePhotos(delPhotos);
|
this.deletePhotos(delPhotos);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open the edit date dialog
|
* Open the edit date dialog
|
||||||
*/
|
*/
|
||||||
private async editDateSelection(selection: Selection) {
|
async editDateSelection(selection: Selection) {
|
||||||
(<any>this.$refs.editDate).open(Array.from(selection.values()));
|
(<any>this.$refs.editDate).open(Array.from(selection.values()));
|
||||||
}
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open the edit date dialog
|
* Open the edit date dialog
|
||||||
*/
|
*/
|
||||||
private async editExifSelection(selection: Selection) {
|
async editExifSelection(selection: Selection) {
|
||||||
if (selection.size !== 1) return;
|
if (selection.size !== 1) return;
|
||||||
(<any>this.$refs.editExif).open(selection.values().next().value);
|
(<any>this.$refs.editExif).open(selection.values().next().value);
|
||||||
}
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open the files app with the selected file (one)
|
* Open the files app with the selected file (one)
|
||||||
* Opens a new window.
|
* Opens a new window.
|
||||||
*/
|
*/
|
||||||
private async viewInFolder(selection: Selection) {
|
async viewInFolder(selection: Selection) {
|
||||||
if (selection.size !== 1) return;
|
if (selection.size !== 1) return;
|
||||||
dav.viewInFolder(selection.values().next().value);
|
dav.viewInFolder(selection.values().next().value);
|
||||||
}
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Archive the currently selected photos
|
* Archive the currently selected photos
|
||||||
*/
|
*/
|
||||||
private async archiveSelection(selection: Selection) {
|
async archiveSelection(selection: Selection) {
|
||||||
if (selection.size >= 100) {
|
if (selection.size >= 100) {
|
||||||
if (
|
if (
|
||||||
!confirm(
|
!confirm(
|
||||||
|
@ -787,19 +793,19 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
|
||||||
const delPhotos = delIds.map((id) => selection.get(id));
|
const delPhotos = delIds.map((id) => selection.get(id));
|
||||||
this.deletePhotos(delPhotos);
|
this.deletePhotos(delPhotos);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move selected photos to album
|
* Move selected photos to album
|
||||||
*/
|
*/
|
||||||
private async addToAlbum(selection: Selection) {
|
async addToAlbum(selection: Selection) {
|
||||||
(<any>this.$refs.addToAlbumModal).open(Array.from(selection.values()));
|
(<any>this.$refs.addToAlbumModal).open(Array.from(selection.values()));
|
||||||
}
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move selected photos to another person
|
* Move selected photos to another person
|
||||||
*/
|
*/
|
||||||
private async moveSelectionToPerson(selection: Selection) {
|
async moveSelectionToPerson(selection: Selection) {
|
||||||
if (!this.config_showFaceRect) {
|
if (!this.config_showFaceRect) {
|
||||||
showError(
|
showError(
|
||||||
this.t(
|
this.t(
|
||||||
|
@ -810,12 +816,12 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
(<any>this.$refs.faceMoveModal).open(Array.from(selection.values()));
|
(<any>this.$refs.faceMoveModal).open(Array.from(selection.values()));
|
||||||
}
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove currently selected photos from person
|
* Remove currently selected photos from person
|
||||||
*/
|
*/
|
||||||
private async removeSelectionFromPerson(selection: Selection) {
|
async removeSelectionFromPerson(selection: Selection) {
|
||||||
// Make sure route is valid
|
// Make sure route is valid
|
||||||
const { user, name } = this.$route.params;
|
const { user, name } = this.$route.params;
|
||||||
if (this.$route.name !== "recognize" || !user || !name) {
|
if (this.$route.name !== "recognize" || !user || !name) {
|
||||||
|
@ -838,20 +844,23 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
|
||||||
name,
|
name,
|
||||||
Array.from(selection.values())
|
Array.from(selection.values())
|
||||||
)) {
|
)) {
|
||||||
const delPhotos = delIds.filter((x) => x).map((id) => selection.get(id));
|
const delPhotos = delIds
|
||||||
|
.filter((x) => x)
|
||||||
|
.map((id) => selection.get(id));
|
||||||
this.deletePhotos(delPhotos);
|
this.deletePhotos(delPhotos);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Open viewer with given photo */
|
/** Open viewer with given photo */
|
||||||
private openViewer(photo: IPhoto) {
|
openViewer(photo: IPhoto) {
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
path: this.$route.path,
|
path: this.$route.path,
|
||||||
query: this.$route.query,
|
query: this.$route.query,
|
||||||
hash: utils.getViewerHash(photo),
|
hash: utils.getViewerHash(photo),
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -69,9 +69,7 @@ input[type="text"] {
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Mixins } from "vue-property-decorator";
|
import { defineComponent } from "vue";
|
||||||
import GlobalMixin from "../mixins/GlobalMixin";
|
|
||||||
import UserConfig from "../mixins/UserConfig";
|
|
||||||
|
|
||||||
import { getFilePickerBuilder } from "@nextcloud/dialogs";
|
import { getFilePickerBuilder } from "@nextcloud/dialogs";
|
||||||
const NcCheckboxRadioSwitch = () =>
|
const NcCheckboxRadioSwitch = () =>
|
||||||
|
@ -79,17 +77,21 @@ const NcCheckboxRadioSwitch = () =>
|
||||||
|
|
||||||
import MultiPathSelectionModal from "./modal/MultiPathSelectionModal.vue";
|
import MultiPathSelectionModal from "./modal/MultiPathSelectionModal.vue";
|
||||||
|
|
||||||
@Component({
|
export default defineComponent({
|
||||||
|
name: "Settings",
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
NcCheckboxRadioSwitch,
|
NcCheckboxRadioSwitch,
|
||||||
MultiPathSelectionModal,
|
MultiPathSelectionModal,
|
||||||
},
|
},
|
||||||
})
|
|
||||||
export default class Settings extends Mixins(UserConfig, GlobalMixin) {
|
|
||||||
get pathSelTitle() {
|
|
||||||
return this.t("memories", "Choose Timeline Paths");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
pathSelTitle() {
|
||||||
|
return this.t("memories", "Choose Timeline Paths");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
async chooseFolder(title: string, initial: string) {
|
async chooseFolder(title: string, initial: string) {
|
||||||
const picker = getFilePickerBuilder(title)
|
const picker = getFilePickerBuilder(title)
|
||||||
.setMultiSelect(false)
|
.setMultiSelect(false)
|
||||||
|
@ -101,11 +103,13 @@ export default class Settings extends Mixins(UserConfig, GlobalMixin) {
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
return await picker.pick();
|
return await picker.pick();
|
||||||
}
|
},
|
||||||
|
|
||||||
async chooseTimelinePath() {
|
async chooseTimelinePath() {
|
||||||
(<any>this.$refs.multiPathModal).open(this.config_timelinePath.split(";"));
|
(<any>this.$refs.multiPathModal).open(
|
||||||
}
|
this.config_timelinePath.split(";")
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
async saveTimelinePath(paths: string[]) {
|
async saveTimelinePath(paths: string[]) {
|
||||||
if (!paths || !paths.length) return;
|
if (!paths || !paths.length) return;
|
||||||
|
@ -115,7 +119,7 @@ export default class Settings extends Mixins(UserConfig, GlobalMixin) {
|
||||||
this.config_timelinePath = newPath;
|
this.config_timelinePath = newPath;
|
||||||
await this.updateSetting("timelinePath");
|
await this.updateSetting("timelinePath");
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
async chooseFoldersPath() {
|
async chooseFoldersPath() {
|
||||||
let newPath = await this.chooseFolder(
|
let newPath = await this.chooseFolder(
|
||||||
|
@ -127,14 +131,15 @@ export default class Settings extends Mixins(UserConfig, GlobalMixin) {
|
||||||
this.config_foldersPath = newPath;
|
this.config_foldersPath = newPath;
|
||||||
await this.updateSetting("foldersPath");
|
await this.updateSetting("foldersPath");
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
async updateSquareThumbs() {
|
async updateSquareThumbs() {
|
||||||
await this.updateSetting("squareThumbs");
|
await this.updateSetting("squareThumbs");
|
||||||
}
|
},
|
||||||
|
|
||||||
async updateShowHidden() {
|
async updateShowHidden() {
|
||||||
await this.updateSetting("showHidden");
|
await this.updateSetting("showHidden");
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
|
});
|
||||||
</script>
|
</script>
|
|
@ -141,9 +141,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Mixins, Watch } from "vue-property-decorator";
|
import { defineComponent } from "vue";
|
||||||
import GlobalMixin from "../mixins/GlobalMixin";
|
|
||||||
import UserConfig from "../mixins/UserConfig";
|
|
||||||
|
|
||||||
import axios from "@nextcloud/axios";
|
import axios from "@nextcloud/axios";
|
||||||
import { showError } from "@nextcloud/dialogs";
|
import { showError } from "@nextcloud/dialogs";
|
||||||
|
@ -174,7 +172,9 @@ const SCROLL_LOAD_DELAY = 100; // Delay in loading data when scrolling
|
||||||
const DESKTOP_ROW_HEIGHT = 200; // Height of row on desktop
|
const DESKTOP_ROW_HEIGHT = 200; // Height of row on desktop
|
||||||
const MOBILE_ROW_HEIGHT = 120; // Approx row height on mobile
|
const MOBILE_ROW_HEIGHT = 120; // Approx row height on mobile
|
||||||
|
|
||||||
@Component({
|
export default defineComponent({
|
||||||
|
name: "Timeline",
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
Folder,
|
Folder,
|
||||||
Tag,
|
Tag,
|
||||||
|
@ -191,55 +191,144 @@ const MOBILE_ROW_HEIGHT = 120; // Approx row height on mobile
|
||||||
PeopleIcon,
|
PeopleIcon,
|
||||||
ImageMultipleIcon,
|
ImageMultipleIcon,
|
||||||
},
|
},
|
||||||
})
|
|
||||||
export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
data() {
|
||||||
|
return {
|
||||||
/** Loading days response */
|
/** Loading days response */
|
||||||
private loading = 0;
|
loading: 0,
|
||||||
/** Main list of rows */
|
/** Main list of rows */
|
||||||
private list: IRow[] = [];
|
list: [] as IRow[],
|
||||||
/** Computed number of columns */
|
/** Computed number of columns */
|
||||||
private numCols = 0;
|
numCols: 0,
|
||||||
/** Header rows for dayId key */
|
/** Header rows for dayId key */
|
||||||
private heads: { [dayid: number]: IHeadRow } = {};
|
heads: {} as { [dayid: number]: IHeadRow },
|
||||||
|
|
||||||
/** Computed row height */
|
/** Computed row height */
|
||||||
private rowHeight = 100;
|
rowHeight: 100,
|
||||||
/** Computed row width */
|
/** Computed row width */
|
||||||
private rowWidth = 100;
|
rowWidth: 100,
|
||||||
|
|
||||||
/** Current start index */
|
/** Current start index */
|
||||||
private currentStart = 0;
|
currentStart: 0,
|
||||||
/** Current end index */
|
/** Current end index */
|
||||||
private currentEnd = 0;
|
currentEnd: 0,
|
||||||
/** Resizing timer */
|
/** Resizing timer */
|
||||||
private resizeTimer = null as number | null;
|
resizeTimer: null as number | null,
|
||||||
/** Height of the scroller */
|
/** Height of the scroller */
|
||||||
private scrollerHeight = 100;
|
scrollerHeight: 100,
|
||||||
|
|
||||||
/** Set of dayIds for which images loaded */
|
/** Set of dayIds for which images loaded */
|
||||||
private loadedDays = new Set<number>();
|
loadedDays: new Set<number>(),
|
||||||
/** Set of dayIds for which image size is calculated */
|
/** Set of dayIds for which image size is calculated */
|
||||||
private sizedDays = new Set<number>();
|
sizedDays: new Set<number>(),
|
||||||
/** Days to load in the next call */
|
/** Days to load in the next call */
|
||||||
private fetchDayQueue = [] as number[];
|
fetchDayQueue: [] as number[],
|
||||||
/** Timer to load day call */
|
/** Timer to load day call */
|
||||||
private fetchDayTimer = null as number | null;
|
fetchDayTimer: null as number | null,
|
||||||
|
|
||||||
/** State for request cancellations */
|
/** State for request cancellations */
|
||||||
private state = Math.random();
|
state: Math.random(),
|
||||||
|
|
||||||
/** Selection manager component */
|
/** Selection manager component */
|
||||||
private selectionManager!: SelectionManager & any;
|
selectionManager: null as SelectionManager & any,
|
||||||
/** Scroller manager component */
|
/** Scroller manager component */
|
||||||
private scrollerManager!: ScrollerManager & any;
|
scrollerManager: null as ScrollerManager & any,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.selectionManager = this.$refs.selectionManager;
|
this.selectionManager = this.$refs.selectionManager;
|
||||||
this.scrollerManager = this.$refs.scrollerManager;
|
this.scrollerManager = this.$refs.scrollerManager;
|
||||||
this.routeChange(this.$route);
|
this.routeChange(this.$route);
|
||||||
}
|
},
|
||||||
|
|
||||||
@Watch("$route")
|
watch: {
|
||||||
|
async $route(to: any, from?: any) {
|
||||||
|
await this.routeChange(to, from);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeDestroy() {
|
||||||
|
unsubscribe(this.config_eventName, this.softRefresh);
|
||||||
|
unsubscribe("files:file:created", this.softRefresh);
|
||||||
|
this.resetState();
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
subscribe(this.config_eventName, this.softRefresh);
|
||||||
|
subscribe("files:file:created", this.softRefresh);
|
||||||
|
window.addEventListener("resize", this.handleResizeWithDelay);
|
||||||
|
},
|
||||||
|
|
||||||
|
destroyed() {
|
||||||
|
window.removeEventListener("resize", this.handleResizeWithDelay);
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
routeIsBase() {
|
||||||
|
return this.$route.name === "timeline";
|
||||||
|
},
|
||||||
|
routeIsPeople() {
|
||||||
|
return ["recognize", "facerecognition"].includes(this.$route.name);
|
||||||
|
},
|
||||||
|
routeIsArchive() {
|
||||||
|
return this.$route.name === "archive";
|
||||||
|
},
|
||||||
|
isMonthView() {
|
||||||
|
return this.$route.name === "albums";
|
||||||
|
},
|
||||||
|
/** Get view name for dynamic top matter */
|
||||||
|
viewName() {
|
||||||
|
switch (this.$route.name) {
|
||||||
|
case "timeline":
|
||||||
|
return this.t("memories", "Your Timeline");
|
||||||
|
case "favorites":
|
||||||
|
return this.t("memories", "Favorites");
|
||||||
|
case "recognize":
|
||||||
|
case "facerecognition":
|
||||||
|
return this.t("memories", "People");
|
||||||
|
case "videos":
|
||||||
|
return this.t("memories", "Videos");
|
||||||
|
case "albums":
|
||||||
|
return this.t("memories", "Albums");
|
||||||
|
case "archive":
|
||||||
|
return this.t("memories", "Archive");
|
||||||
|
case "thisday":
|
||||||
|
return this.t("memories", "On this day");
|
||||||
|
case "tags":
|
||||||
|
return this.t("memories", "Tags");
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emptyViewDescription() {
|
||||||
|
switch (this.$route.name) {
|
||||||
|
case "facerecognition":
|
||||||
|
if (this.config_facerecognitionEnabled)
|
||||||
|
return this.t(
|
||||||
|
"memories",
|
||||||
|
"You will find your friends soon. Please, be patient."
|
||||||
|
);
|
||||||
|
else
|
||||||
|
return this.t(
|
||||||
|
"memories",
|
||||||
|
"Face Recognition is disabled. Enable in settings to find your friends."
|
||||||
|
);
|
||||||
|
case "timeline":
|
||||||
|
case "favorites":
|
||||||
|
case "recognize":
|
||||||
|
case "videos":
|
||||||
|
case "albums":
|
||||||
|
case "archive":
|
||||||
|
case "thisday":
|
||||||
|
case "tags":
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
async routeChange(to: any, from?: any) {
|
async routeChange(to: any, from?: any) {
|
||||||
if (from?.path !== to.path) {
|
if (from?.path !== to.path) {
|
||||||
await this.refresh();
|
await this.refresh();
|
||||||
|
@ -250,7 +339,11 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
||||||
|
|
||||||
// Check if hash has changed
|
// Check if hash has changed
|
||||||
const viewerIsOpen = (this.$refs.viewer as any).isOpen;
|
const viewerIsOpen = (this.$refs.viewer as any).isOpen;
|
||||||
if (from?.hash !== to.hash && to.hash?.startsWith("#v") && !viewerIsOpen) {
|
if (
|
||||||
|
from?.hash !== to.hash &&
|
||||||
|
to.hash?.startsWith("#v") &&
|
||||||
|
!viewerIsOpen
|
||||||
|
) {
|
||||||
// Open viewer
|
// Open viewer
|
||||||
const parts = to.hash.split("/");
|
const parts = to.hash.split("/");
|
||||||
if (parts.length !== 3) return;
|
if (parts.length !== 3) return;
|
||||||
|
@ -291,55 +384,22 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
||||||
// Close viewer
|
// Close viewer
|
||||||
(this.$refs.viewer as any).close();
|
(this.$refs.viewer as any).close();
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
beforeDestroy() {
|
|
||||||
unsubscribe(this.config_eventName, this.softRefresh);
|
|
||||||
unsubscribe("files:file:created", this.softRefresh);
|
|
||||||
this.resetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
created() {
|
|
||||||
subscribe(this.config_eventName, this.softRefresh);
|
|
||||||
subscribe("files:file:created", this.softRefresh);
|
|
||||||
window.addEventListener("resize", this.handleResizeWithDelay);
|
|
||||||
}
|
|
||||||
|
|
||||||
destroyed() {
|
|
||||||
window.removeEventListener("resize", this.handleResizeWithDelay);
|
|
||||||
}
|
|
||||||
|
|
||||||
get routeIsBase() {
|
|
||||||
return this.$route.name === "timeline";
|
|
||||||
}
|
|
||||||
|
|
||||||
get routeIsPeople() {
|
|
||||||
return ["recognize", "facerecognition"].includes(this.$route.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
get routeIsArchive() {
|
|
||||||
return this.$route.name === "archive";
|
|
||||||
}
|
|
||||||
|
|
||||||
updateLoading(delta: number) {
|
updateLoading(delta: number) {
|
||||||
this.loading += delta;
|
this.loading += delta;
|
||||||
}
|
},
|
||||||
|
|
||||||
isMobile() {
|
isMobile() {
|
||||||
return globalThis.windowInnerWidth <= 768;
|
return globalThis.windowInnerWidth <= 768;
|
||||||
}
|
},
|
||||||
|
|
||||||
isMobileLayout() {
|
isMobileLayout() {
|
||||||
return globalThis.windowInnerWidth <= 600;
|
return globalThis.windowInnerWidth <= 600;
|
||||||
}
|
},
|
||||||
|
|
||||||
get isMonthView() {
|
|
||||||
return this.$route.name === "albums";
|
|
||||||
}
|
|
||||||
|
|
||||||
allowBreakout() {
|
allowBreakout() {
|
||||||
return this.isMobileLayout() && !this.config_squareThumbs;
|
return this.isMobileLayout() && !this.config_squareThumbs;
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Create new state */
|
/** Create new state */
|
||||||
async createState() {
|
async createState() {
|
||||||
|
@ -358,7 +418,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
||||||
|
|
||||||
// Get data
|
// Get data
|
||||||
await this.fetchDays();
|
await this.fetchDays();
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Reset all state */
|
/** Reset all state */
|
||||||
async resetState() {
|
async resetState() {
|
||||||
|
@ -375,19 +435,19 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
||||||
this.fetchDayQueue = [];
|
this.fetchDayQueue = [];
|
||||||
window.clearTimeout(this.fetchDayTimer);
|
window.clearTimeout(this.fetchDayTimer);
|
||||||
window.clearTimeout(this.resizeTimer);
|
window.clearTimeout(this.resizeTimer);
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Recreate everything */
|
/** Recreate everything */
|
||||||
async refresh() {
|
async refresh() {
|
||||||
await this.resetState();
|
await this.resetState();
|
||||||
await this.createState();
|
await this.createState();
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Re-process days */
|
/** Re-process days */
|
||||||
async softRefresh() {
|
async softRefresh() {
|
||||||
this.selectionManager.clearSelection();
|
this.selectionManager.clearSelection();
|
||||||
await this.fetchDays(true);
|
await this.fetchDays(true);
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Do resize after some time */
|
/** Do resize after some time */
|
||||||
handleResizeWithDelay() {
|
handleResizeWithDelay() {
|
||||||
|
@ -403,7 +463,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
||||||
this.recomputeSizes();
|
this.recomputeSizes();
|
||||||
this.resizeTimer = null;
|
this.resizeTimer = null;
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Recompute static sizes of containers */
|
/** Recompute static sizes of containers */
|
||||||
recomputeSizes() {
|
recomputeSizes() {
|
||||||
|
@ -442,7 +502,10 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
||||||
|
|
||||||
if (this.isMobileLayout()) {
|
if (this.isMobileLayout()) {
|
||||||
// Mobile
|
// Mobile
|
||||||
this.numCols = Math.max(3, Math.floor(this.rowWidth / MOBILE_ROW_HEIGHT));
|
this.numCols = Math.max(
|
||||||
|
3,
|
||||||
|
Math.floor(this.rowWidth / MOBILE_ROW_HEIGHT)
|
||||||
|
);
|
||||||
this.rowHeight = Math.floor(this.rowWidth / this.numCols);
|
this.rowHeight = Math.floor(this.rowWidth / this.numCols);
|
||||||
} else {
|
} else {
|
||||||
// Desktop
|
// Desktop
|
||||||
|
@ -470,7 +533,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
||||||
// Explicitly request a scroll event
|
// Explicitly request a scroll event
|
||||||
this.loadScrollChanges(this.currentStart, this.currentEnd);
|
this.loadScrollChanges(this.currentStart, this.currentEnd);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Triggered when position of scroll change.
|
* Triggered when position of scroll change.
|
||||||
|
@ -479,7 +542,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
||||||
*/
|
*/
|
||||||
scrollPositionChange(event?: any) {
|
scrollPositionChange(event?: any) {
|
||||||
this.scrollerManager.recyclerScrolled(event);
|
this.scrollerManager.recyclerScrolled(event);
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Trigger when recycler view changes */
|
/** Trigger when recycler view changes */
|
||||||
scrollChange(startIndex: number, endIndex: number) {
|
scrollChange(startIndex: number, endIndex: number) {
|
||||||
|
@ -537,7 +600,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
||||||
this.loadScrollChanges(start, end);
|
this.loadScrollChanges(start, end);
|
||||||
}
|
}
|
||||||
}, SCROLL_LOAD_DELAY);
|
}, SCROLL_LOAD_DELAY);
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Load image data for given view */
|
/** Load image data for given view */
|
||||||
loadScrollChanges(startIndex: number, endIndex: number) {
|
loadScrollChanges(startIndex: number, endIndex: number) {
|
||||||
|
@ -559,7 +622,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
||||||
|
|
||||||
this.fetchDay(item.dayId);
|
this.fetchDay(item.dayId);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Get query string for API calls */
|
/** Get query string for API calls */
|
||||||
getQuery() {
|
getQuery() {
|
||||||
|
@ -622,32 +685,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return query;
|
return query;
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Get view name for dynamic top matter */
|
|
||||||
get viewName() {
|
|
||||||
switch (this.$route.name) {
|
|
||||||
case "timeline":
|
|
||||||
return this.t("memories", "Your Timeline");
|
|
||||||
case "favorites":
|
|
||||||
return this.t("memories", "Favorites");
|
|
||||||
case "recognize":
|
|
||||||
case "facerecognition":
|
|
||||||
return this.t("memories", "People");
|
|
||||||
case "videos":
|
|
||||||
return this.t("memories", "Videos");
|
|
||||||
case "albums":
|
|
||||||
return this.t("memories", "Albums");
|
|
||||||
case "archive":
|
|
||||||
return this.t("memories", "Archive");
|
|
||||||
case "thisday":
|
|
||||||
return this.t("memories", "On this day");
|
|
||||||
case "tags":
|
|
||||||
return this.t("memories", "Tags");
|
|
||||||
default:
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get name of header */
|
/** Get name of header */
|
||||||
getHeadName(head: IHeadRow) {
|
getHeadName(head: IHeadRow) {
|
||||||
|
@ -675,34 +713,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
||||||
// Cache and return
|
// Cache and return
|
||||||
head.name = name;
|
head.name = name;
|
||||||
return head.name;
|
return head.name;
|
||||||
}
|
},
|
||||||
|
|
||||||
/* Get a friendly description of empty view */
|
|
||||||
get emptyViewDescription() {
|
|
||||||
switch (this.$route.name) {
|
|
||||||
case "facerecognition":
|
|
||||||
if (this.config_facerecognitionEnabled)
|
|
||||||
return this.t(
|
|
||||||
"memories",
|
|
||||||
"You will find your friends soon. Please, be patient."
|
|
||||||
);
|
|
||||||
else
|
|
||||||
return this.t(
|
|
||||||
"memories",
|
|
||||||
"Face Recognition is disabled. Enable in settings to find your friends."
|
|
||||||
);
|
|
||||||
case "timeline":
|
|
||||||
case "favorites":
|
|
||||||
case "recognize":
|
|
||||||
case "videos":
|
|
||||||
case "albums":
|
|
||||||
case "archive":
|
|
||||||
case "thisday":
|
|
||||||
case "tags":
|
|
||||||
default:
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Fetch timeline main call */
|
/** Fetch timeline main call */
|
||||||
async fetchDays(noCache = false) {
|
async fetchDays(noCache = false) {
|
||||||
|
@ -759,7 +770,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
||||||
} finally {
|
} finally {
|
||||||
if (!cache) this.loading--;
|
if (!cache) this.loading--;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Process the data for days call including folders */
|
/** Process the data for days call including folders */
|
||||||
async processDays(data: IDay[]) {
|
async processDays(data: IDay[]) {
|
||||||
|
@ -868,12 +879,12 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
||||||
// Fix view height variable
|
// Fix view height variable
|
||||||
await this.scrollerManager.reflow();
|
await this.scrollerManager.reflow();
|
||||||
this.scrollPositionChange();
|
this.scrollPositionChange();
|
||||||
}
|
},
|
||||||
|
|
||||||
/** API url for Day call */
|
/** API url for Day call */
|
||||||
private getDayUrl(dayId: number | string) {
|
getDayUrl(dayId: number | string) {
|
||||||
return API.Q(API.DAY(dayId), this.getQuery());
|
return API.Q(API.DAY(dayId), this.getQuery());
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Fetch image data for one dayId */
|
/** Fetch image data for one dayId */
|
||||||
async fetchDay(dayId: number, now = false) {
|
async fetchDay(dayId: number, now = false) {
|
||||||
|
@ -907,7 +918,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
||||||
this.fetchDayExpire();
|
this.fetchDayExpire();
|
||||||
}, 150);
|
}, 150);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
async fetchDayExpire() {
|
async fetchDayExpire() {
|
||||||
if (this.fetchDayQueue.length === 0) return;
|
if (this.fetchDayQueue.length === 0) return;
|
||||||
|
@ -966,7 +977,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
||||||
showError(this.t("memories", "Failed to load some photos"));
|
showError(this.t("memories", "Failed to load some photos"));
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process items from day response.
|
* Process items from day response.
|
||||||
|
@ -992,7 +1003,8 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
||||||
(p) =>
|
(p) =>
|
||||||
!(
|
!(
|
||||||
p.flag & this.c.FLAG_IS_FOLDER &&
|
p.flag & this.c.FLAG_IS_FOLDER &&
|
||||||
((<IFolder>p).name.startsWith(".") || !(<IFolder>p).previews.length)
|
((<IFolder>p).name.startsWith(".") ||
|
||||||
|
!(<IFolder>p).previews.length)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1209,7 +1221,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
||||||
// Scroll to new position
|
// Scroll to new position
|
||||||
(<any>this.$refs.recycler).$el.scrollTop = scrollTop;
|
(<any>this.$refs.recycler).$el.scrollTop = scrollTop;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
/** Add and get a new blank photos row */
|
/** Add and get a new blank photos row */
|
||||||
addRow(day: IDay): IRow {
|
addRow(day: IDay): IRow {
|
||||||
|
@ -1233,7 +1245,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
||||||
day.rows.push(row);
|
day.rows.push(row);
|
||||||
|
|
||||||
return row;
|
return row;
|
||||||
}
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete elements from main view with some animation
|
* Delete elements from main view with some animation
|
||||||
|
@ -1269,8 +1281,9 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
||||||
const newDetail = day.detail.filter((p) => !delPhotosSet.has(p));
|
const newDetail = day.detail.filter((p) => !delPhotosSet.has(p));
|
||||||
this.processDay(day.dayid, newDetail);
|
this.processDay(day.dayid, newDetail);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -29,41 +29,66 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Watch, Mixins } from "vue-property-decorator";
|
import { defineComponent, PropType } from "vue";
|
||||||
import { IFolder, IPhoto } from "../../types";
|
import { IFolder, IPhoto } from "../../types";
|
||||||
import GlobalMixin from "../../mixins/GlobalMixin";
|
|
||||||
import UserConfig from "../../mixins/UserConfig";
|
|
||||||
|
|
||||||
import { getPreviewUrl } from "../../services/FileUtils";
|
import { getPreviewUrl } from "../../services/FileUtils";
|
||||||
|
|
||||||
import FolderIcon from "vue-material-design-icons/Folder.vue";
|
import FolderIcon from "vue-material-design-icons/Folder.vue";
|
||||||
|
|
||||||
@Component({
|
export default defineComponent({
|
||||||
|
name: "Folder",
|
||||||
components: {
|
components: {
|
||||||
FolderIcon,
|
FolderIcon,
|
||||||
},
|
},
|
||||||
})
|
|
||||||
export default class Folder extends Mixins(GlobalMixin, UserConfig) {
|
|
||||||
@Prop() data: IFolder;
|
|
||||||
|
|
||||||
|
props: {
|
||||||
|
data: Object as PropType<IFolder>,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
// Separate property because the one on data isn't reactive
|
// Separate property because the one on data isn't reactive
|
||||||
private previews: IPhoto[] = [];
|
previews: [] as IPhoto[],
|
||||||
|
|
||||||
// Error occured fetching thumbs
|
// Error occured fetching thumbs
|
||||||
private error = false;
|
error: false,
|
||||||
|
// Passthrough
|
||||||
|
getPreviewUrl,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
/** Passthrough */
|
computed: {
|
||||||
private getPreviewUrl = getPreviewUrl;
|
/** Open folder */
|
||||||
|
target() {
|
||||||
|
const path = this.data.path
|
||||||
|
.split("/")
|
||||||
|
.filter((x) => x)
|
||||||
|
.slice(2) as string[];
|
||||||
|
|
||||||
|
// Remove base path if present
|
||||||
|
const basePath = this.config_foldersPath.split("/").filter((x) => x);
|
||||||
|
if (
|
||||||
|
path.length >= basePath.length &&
|
||||||
|
path.slice(0, basePath.length).every((x, i) => x === basePath[i])
|
||||||
|
) {
|
||||||
|
path.splice(0, basePath.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { name: "folders", params: { path: path as any } };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.refreshPreviews();
|
this.refreshPreviews();
|
||||||
}
|
},
|
||||||
|
|
||||||
@Watch("data")
|
watch: {
|
||||||
dataChanged() {
|
data() {
|
||||||
this.refreshPreviews();
|
this.refreshPreviews();
|
||||||
}
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
/** Refresh previews */
|
/** Refresh previews */
|
||||||
refreshPreviews() {
|
refreshPreviews() {
|
||||||
// Reset state
|
// Reset state
|
||||||
|
@ -84,27 +109,9 @@ export default class Folder extends Mixins(GlobalMixin, UserConfig) {
|
||||||
this.previews = previews.slice(0, 4);
|
this.previews = previews.slice(0, 4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
},
|
||||||
/** Open folder */
|
});
|
||||||
get target() {
|
|
||||||
const path = this.data.path
|
|
||||||
.split("/")
|
|
||||||
.filter((x) => x)
|
|
||||||
.slice(2) as string[];
|
|
||||||
|
|
||||||
// Remove base path if present
|
|
||||||
const basePath = this.config_foldersPath.split("/").filter((x) => x);
|
|
||||||
if (
|
|
||||||
path.length >= basePath.length &&
|
|
||||||
path.slice(0, basePath.length).every((x, i) => x === basePath[i])
|
|
||||||
) {
|
|
||||||
path.splice(0, basePath.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { name: "folders", params: { path: path as any } };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -4,6 +4,8 @@ import "reflect-metadata";
|
||||||
import Vue from "vue";
|
import Vue from "vue";
|
||||||
import VueVirtualScroller from "vue-virtual-scroller";
|
import VueVirtualScroller from "vue-virtual-scroller";
|
||||||
import "vue-virtual-scroller/dist/vue-virtual-scroller.css";
|
import "vue-virtual-scroller/dist/vue-virtual-scroller.css";
|
||||||
|
import GlobalMixin from "./mixins/GlobalMixin";
|
||||||
|
import UserConfig from "./mixins/UserConfig";
|
||||||
|
|
||||||
import App from "./App.vue";
|
import App from "./App.vue";
|
||||||
import router from "./router";
|
import router from "./router";
|
||||||
|
@ -64,6 +66,8 @@ if (!globalThis.videoClientIdPersistent) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Vue.mixin(GlobalMixin);
|
||||||
|
Vue.mixin(UserConfig);
|
||||||
Vue.use(VueVirtualScroller);
|
Vue.use(VueVirtualScroller);
|
||||||
|
|
||||||
// https://github.com/nextcloud/photos/blob/156f280c0476c483cb9ce81769ccb0c1c6500a4e/src/main.js
|
// https://github.com/nextcloud/photos/blob/156f280c0476c483cb9ce81769ccb0c1c6500a4e/src/main.js
|
||||||
|
|
Loading…
Reference in New Issue