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