refactor: tag frame to cluster
Signed-off-by: Varun Patil <varunpatil@ucla.edu>pull/563/head
parent
92781df0c1
commit
7326ee8ec0
|
@ -0,0 +1,137 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="noParams" class="container" :class="{ 'icon-loading': loading }">
|
||||||
|
<TopMatter />
|
||||||
|
|
||||||
|
<EmptyContent v-if="items.length === 0 && !loading" />
|
||||||
|
|
||||||
|
<RecycleScroller
|
||||||
|
class="grid-recycler hide-scrollbar-mobile"
|
||||||
|
:class="{ empty: !items.length }"
|
||||||
|
ref="recycler"
|
||||||
|
:items="items"
|
||||||
|
:skipHover="true"
|
||||||
|
:itemSize="itemSize"
|
||||||
|
:gridItems="gridItems"
|
||||||
|
:updateInterval="100"
|
||||||
|
key-field="cluster_id"
|
||||||
|
@resize="resize"
|
||||||
|
>
|
||||||
|
<template v-slot="{ item }">
|
||||||
|
<div class="grid-item fill-block" :key="item.cluster_id">
|
||||||
|
<Cluster :data="item" @click="click(item)" :link="link" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</RecycleScroller>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Timeline v-else />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
|
||||||
|
import UserConfig from "../mixins/UserConfig";
|
||||||
|
import TopMatter from "./top-matter/TopMatter.vue";
|
||||||
|
import Cluster from "./frame/Cluster.vue";
|
||||||
|
import Timeline from "./Timeline.vue";
|
||||||
|
import EmptyContent from "./top-matter/EmptyContent.vue";
|
||||||
|
|
||||||
|
import * as dav from "../services/DavRequests";
|
||||||
|
|
||||||
|
import { ICluster } from "../types";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "ClusterView",
|
||||||
|
|
||||||
|
components: {
|
||||||
|
TopMatter,
|
||||||
|
Cluster,
|
||||||
|
Timeline,
|
||||||
|
EmptyContent,
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [UserConfig],
|
||||||
|
|
||||||
|
data: () => ({
|
||||||
|
items: [] as ICluster[],
|
||||||
|
itemSize: 200,
|
||||||
|
gridItems: 5,
|
||||||
|
loading: 0,
|
||||||
|
}),
|
||||||
|
|
||||||
|
props: {
|
||||||
|
link: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
noParams() {
|
||||||
|
return !this.$route.params.name && !this.$route.params.user;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.routeChange(this.$route);
|
||||||
|
this.resize();
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
async $route(to: any, from?: any) {
|
||||||
|
this.routeChange(to, from);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async routeChange(to: any, from?: any) {
|
||||||
|
try {
|
||||||
|
this.items = [];
|
||||||
|
this.loading++;
|
||||||
|
|
||||||
|
if (to.name === "albums") {
|
||||||
|
this.items = await dav.getAlbums(3, this.config_albumListSort);
|
||||||
|
} else if (to.name === "tags") {
|
||||||
|
this.items = await dav.getTags();
|
||||||
|
} else if (to.name === "recognize" || to.name === "facerecognition") {
|
||||||
|
this.items = await dav.getFaceList(to.name);
|
||||||
|
} else if (to.name === "places") {
|
||||||
|
this.items = await dav.getPlaces();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.loading--;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
click(item: ICluster) {
|
||||||
|
this.$emit("click", item);
|
||||||
|
},
|
||||||
|
|
||||||
|
resize() {
|
||||||
|
const w = (<any>this.$refs.recycler).$el.clientWidth;
|
||||||
|
this.gridItems = Math.max(Math.floor(w / 200), 3);
|
||||||
|
this.itemSize = Math.floor(w / this.gridItems);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.container {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.grid-recycler {
|
||||||
|
flex: 1;
|
||||||
|
max-height: 100%;
|
||||||
|
overflow-y: scroll !important;
|
||||||
|
&.empty {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.grid-item {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -528,8 +528,7 @@ export default defineComponent({
|
||||||
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
|
||||||
photo.flag & this.c.FLAG_IS_TAG
|
|
||||||
) {
|
) {
|
||||||
return; // ignore placeholders
|
return; // ignore placeholders
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,22 +8,12 @@
|
||||||
<TopMatter ref="topmatter" />
|
<TopMatter ref="topmatter" />
|
||||||
|
|
||||||
<!-- No content found and nothing is loading -->
|
<!-- No content found and nothing is loading -->
|
||||||
<NcEmptyContent
|
<EmptyContent v-if="loading === 0 && list.length === 0" />
|
||||||
title="Nothing to show here"
|
|
||||||
:description="emptyViewDescription"
|
|
||||||
v-if="loading === 0 && list.length === 0"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<PeopleIcon v-if="routeIsPeople" />
|
|
||||||
<ArchiveIcon v-else-if="routeIsArchive" />
|
|
||||||
<ImageMultipleIcon v-else />
|
|
||||||
</template>
|
|
||||||
</NcEmptyContent>
|
|
||||||
|
|
||||||
<!-- Main recycler view for rows -->
|
<!-- Main recycler view for rows -->
|
||||||
<RecycleScroller
|
<RecycleScroller
|
||||||
ref="recycler"
|
ref="recycler"
|
||||||
class="recycler"
|
class="recycler hide-scrollbar"
|
||||||
:class="{ empty: list.length === 0 }"
|
:class="{ empty: list.length === 0 }"
|
||||||
:items="list"
|
:items="list"
|
||||||
:emit-update="true"
|
:emit-update="true"
|
||||||
|
@ -83,8 +73,6 @@
|
||||||
>
|
>
|
||||||
<Folder v-if="photo.flag & c.FLAG_IS_FOLDER" :data="photo" />
|
<Folder v-if="photo.flag & c.FLAG_IS_FOLDER" :data="photo" />
|
||||||
|
|
||||||
<Tag v-else-if="photo.flag & c.FLAG_IS_TAG" :data="photo" />
|
|
||||||
|
|
||||||
<Photo
|
<Photo
|
||||||
v-else
|
v-else
|
||||||
:data="photo"
|
:data="photo"
|
||||||
|
@ -137,7 +125,6 @@ import { defineComponent } from "vue";
|
||||||
import axios from "@nextcloud/axios";
|
import axios from "@nextcloud/axios";
|
||||||
import { showError } from "@nextcloud/dialogs";
|
import { showError } from "@nextcloud/dialogs";
|
||||||
import { subscribe, unsubscribe } from "@nextcloud/event-bus";
|
import { subscribe, unsubscribe } from "@nextcloud/event-bus";
|
||||||
import NcEmptyContent from "@nextcloud/vue/dist/Components/NcEmptyContent";
|
|
||||||
|
|
||||||
import { getLayout } from "../services/Layout";
|
import { getLayout } from "../services/Layout";
|
||||||
import { IDay, IFolder, IHeadRow, IPhoto, IRow, IRowType } from "../types";
|
import { IDay, IFolder, IHeadRow, IPhoto, IRow, IRowType } from "../types";
|
||||||
|
@ -145,20 +132,19 @@ import { IDay, IFolder, IHeadRow, IPhoto, IRow, IRowType } from "../types";
|
||||||
import UserConfig from "../mixins/UserConfig";
|
import UserConfig from "../mixins/UserConfig";
|
||||||
import Folder from "./frame/Folder.vue";
|
import Folder from "./frame/Folder.vue";
|
||||||
import Photo from "./frame/Photo.vue";
|
import Photo from "./frame/Photo.vue";
|
||||||
import Tag from "./frame/Tag.vue";
|
|
||||||
import ScrollerManager from "./ScrollerManager.vue";
|
import ScrollerManager from "./ScrollerManager.vue";
|
||||||
import SelectionManager from "./SelectionManager.vue";
|
import SelectionManager from "./SelectionManager.vue";
|
||||||
import Viewer from "./viewer/Viewer.vue";
|
import Viewer from "./viewer/Viewer.vue";
|
||||||
|
|
||||||
|
import EmptyContent from "./top-matter/EmptyContent.vue";
|
||||||
import OnThisDay from "./top-matter/OnThisDay.vue";
|
import OnThisDay from "./top-matter/OnThisDay.vue";
|
||||||
import TopMatter from "./top-matter/TopMatter.vue";
|
import TopMatter from "./top-matter/TopMatter.vue";
|
||||||
|
|
||||||
import * as dav from "../services/DavRequests";
|
import * as dav from "../services/DavRequests";
|
||||||
import * as utils from "../services/Utils";
|
import * as utils from "../services/Utils";
|
||||||
|
import * as strings from "../services/strings";
|
||||||
|
|
||||||
import PeopleIcon from "vue-material-design-icons/AccountMultiple.vue";
|
|
||||||
import CheckCircle from "vue-material-design-icons/CheckCircle.vue";
|
import CheckCircle from "vue-material-design-icons/CheckCircle.vue";
|
||||||
import ImageMultipleIcon from "vue-material-design-icons/ImageMultiple.vue";
|
|
||||||
import ArchiveIcon from "vue-material-design-icons/PackageDown.vue";
|
|
||||||
import { API, DaysFilterType } from "../services/API";
|
import { API, DaysFilterType } from "../services/API";
|
||||||
|
|
||||||
const SCROLL_LOAD_DELAY = 100; // Delay in loading data when scrolling
|
const SCROLL_LOAD_DELAY = 100; // Delay in loading data when scrolling
|
||||||
|
@ -170,19 +156,15 @@ export default defineComponent({
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
Folder,
|
Folder,
|
||||||
Tag,
|
|
||||||
Photo,
|
Photo,
|
||||||
TopMatter,
|
EmptyContent,
|
||||||
OnThisDay,
|
OnThisDay,
|
||||||
|
TopMatter,
|
||||||
SelectionManager,
|
SelectionManager,
|
||||||
ScrollerManager,
|
ScrollerManager,
|
||||||
Viewer,
|
Viewer,
|
||||||
NcEmptyContent,
|
|
||||||
|
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
ArchiveIcon,
|
|
||||||
PeopleIcon,
|
|
||||||
ImageMultipleIcon,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [UserConfig],
|
mixins: [UserConfig],
|
||||||
|
@ -279,54 +261,7 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
/** Get view name for dynamic top matter */
|
/** Get view name for dynamic top matter */
|
||||||
viewName(): string {
|
viewName(): string {
|
||||||
switch (this.$route.name) {
|
return strings.viewName(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");
|
|
||||||
case "places":
|
|
||||||
return this.t("memories", "Places");
|
|
||||||
default:
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
},
|
|
||||||
emptyViewDescription(): string {
|
|
||||||
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 "";
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -681,7 +616,11 @@ export default defineComponent({
|
||||||
|
|
||||||
// Map Bounds
|
// Map Bounds
|
||||||
if (this.$route.name === "map" && this.$route.query.b) {
|
if (this.$route.name === "map" && this.$route.query.b) {
|
||||||
API.DAYS_FILTER(query, DaysFilterType.MAP_BOUNDS, <string>this.$route.query.b);
|
API.DAYS_FILTER(
|
||||||
|
query,
|
||||||
|
DaysFilterType.MAP_BOUNDS,
|
||||||
|
<string>this.$route.query.b
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Month view
|
// Month view
|
||||||
|
@ -739,14 +678,6 @@ export default defineComponent({
|
||||||
let data: IDay[] = [];
|
let data: IDay[] = [];
|
||||||
if (this.$route.name === "thisday") {
|
if (this.$route.name === "thisday") {
|
||||||
data = await dav.getOnThisDayData();
|
data = await dav.getOnThisDayData();
|
||||||
} else if (this.$route.name === "albums" && !this.$route.params.name) {
|
|
||||||
data = await dav.getAlbumsData(3, this.config_albumListSort);
|
|
||||||
} else if (this.routeIsPeople && !this.$route.params.name) {
|
|
||||||
data = await dav.getPeopleData(this.$route.name as any);
|
|
||||||
} else if (this.$route.name === "places" && !this.$route.params.name) {
|
|
||||||
data = await dav.getPlacesData();
|
|
||||||
} else if (this.$route.name === "tags" && !this.$route.params.name) {
|
|
||||||
data = await dav.getTagsData();
|
|
||||||
} else if (dav.isSingleItem()) {
|
} else if (dav.isSingleItem()) {
|
||||||
data = await dav.getSingleItemData();
|
data = await dav.getSingleItemData();
|
||||||
this.$router.replace(utils.getViewerRoute(data[0]!.detail[0]));
|
this.$router.replace(utils.getViewerRoute(data[0]!.detail[0]));
|
||||||
|
@ -1043,9 +974,7 @@ export default defineComponent({
|
||||||
return {
|
return {
|
||||||
width: p.w || this.rowHeight,
|
width: p.w || this.rowHeight,
|
||||||
height: p.h || this.rowHeight,
|
height: p.h || this.rowHeight,
|
||||||
forceSquare: Boolean(
|
forceSquare: Boolean(p.flag & this.c.FLAG_IS_FOLDER),
|
||||||
(p.flag & this.c.FLAG_IS_FOLDER) | (p.flag & this.c.FLAG_IS_TAG)
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
|
@ -1419,13 +1348,7 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Static and dynamic top matter */
|
/** Dynamic top matter */
|
||||||
.top-matter {
|
|
||||||
padding-top: 4px;
|
|
||||||
@include phone {
|
|
||||||
padding-left: 40px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.recycler-before {
|
.recycler-before {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
> .text {
|
> .text {
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<router-link
|
<router-link
|
||||||
draggable="false"
|
draggable="false"
|
||||||
class="tag fill-block"
|
class="cluster fill-block"
|
||||||
:class="{ face, error }"
|
:class="{ error }"
|
||||||
:to="target"
|
:to="target"
|
||||||
@click.native="openTag(data)"
|
@click.native="click"
|
||||||
>
|
>
|
||||||
<div class="bbl">
|
<div class="bbl">
|
||||||
<NcCounterBubble> {{ data.count }} </NcCounterBubble>
|
<NcCounterBubble> {{ data.count }} </NcCounterBubble>
|
||||||
|
@ -32,29 +32,30 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, PropType } from "vue";
|
import { defineComponent, PropType } from "vue";
|
||||||
|
|
||||||
import { IAlbum, ITag } from "../../types";
|
import { IAlbum, ICluster, IFace } from "../../types";
|
||||||
import { getPreviewUrl } from "../../services/FileUtils";
|
import { getPreviewUrl } from "../../services/FileUtils";
|
||||||
import { getCurrentUser } from "@nextcloud/auth";
|
import { getCurrentUser } from "@nextcloud/auth";
|
||||||
|
|
||||||
import NcCounterBubble from "@nextcloud/vue/dist/Components/NcCounterBubble";
|
import NcCounterBubble from "@nextcloud/vue/dist/Components/NcCounterBubble";
|
||||||
|
|
||||||
import { constants } from "../../services/Utils";
|
|
||||||
import { API } from "../../services/API";
|
import { API } from "../../services/API";
|
||||||
|
|
||||||
|
import Vue from "vue";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "Tag",
|
name: "Cluster",
|
||||||
components: {
|
components: {
|
||||||
NcCounterBubble,
|
NcCounterBubble,
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
data: {
|
data: {
|
||||||
type: Object as PropType<ITag>,
|
type: Object as PropType<ICluster>,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
noNavigate: {
|
link: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -65,15 +66,7 @@ export default defineComponent({
|
||||||
return getPreviewUrl(mock, true, 512);
|
return getPreviewUrl(mock, true, 512);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.face) {
|
return API.CLUSTER_PREVIEW(this.data.cluster_type, this.data.cluster_id);
|
||||||
return API.FACE_PREVIEW(this.faceApp, this.face.fileid);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.place) {
|
|
||||||
return API.PLACE_PREVIEW(this.place.fileid);
|
|
||||||
}
|
|
||||||
|
|
||||||
return API.TAG_PREVIEW(this.data.name);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
title() {
|
title() {
|
||||||
|
@ -93,35 +86,28 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
|
|
||||||
tag() {
|
tag() {
|
||||||
return !this.face && !this.place && !this.album ? this.data : null;
|
return this.data.cluster_type === "tags" && this.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
face() {
|
face() {
|
||||||
return this.data.flag & constants.c.FLAG_IS_FACE_RECOGNIZE ||
|
return (
|
||||||
this.data.flag & constants.c.FLAG_IS_FACE_RECOGNITION
|
(this.data.cluster_type === "recognize" ||
|
||||||
? this.data
|
this.data.cluster_type === "facerecognition") &&
|
||||||
: null;
|
(this.data as IFace)
|
||||||
},
|
);
|
||||||
|
|
||||||
faceApp() {
|
|
||||||
return this.data.flag & constants.c.FLAG_IS_FACE_RECOGNITION
|
|
||||||
? "facerecognition"
|
|
||||||
: "recognize";
|
|
||||||
},
|
},
|
||||||
|
|
||||||
place() {
|
place() {
|
||||||
return this.data.flag & constants.c.FLAG_IS_PLACE ? this.data : null;
|
return this.data.cluster_type === "places" && this.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
album() {
|
album() {
|
||||||
return this.data.flag & constants.c.FLAG_IS_ALBUM
|
return this.data.cluster_type === "albums" && (this.data as IAlbum);
|
||||||
? <IAlbum>this.data
|
|
||||||
: null;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Target URL to navigate to */
|
/** Target URL to navigate to */
|
||||||
target() {
|
target() {
|
||||||
if (this.noNavigate) return {};
|
if (!this.link) return {};
|
||||||
|
|
||||||
if (this.album) {
|
if (this.album) {
|
||||||
const user = this.album.user;
|
const user = this.album.user;
|
||||||
|
@ -130,13 +116,13 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.face) {
|
if (this.face) {
|
||||||
const name = this.face.name || this.face.fileid.toString();
|
const name = this.face.name || this.face.cluster_id.toString();
|
||||||
const user = this.face.user_id;
|
const user = this.face.user_id;
|
||||||
return { name: this.faceApp, params: { name, user } };
|
return { name: this.data.cluster_type, params: { name, user } };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.place) {
|
if (this.place) {
|
||||||
const id = this.place.fileid.toString();
|
const id = this.place.cluster_id;
|
||||||
const placeName = this.place.name || id;
|
const placeName = this.place.name || id;
|
||||||
const name = `${id}-${placeName}`;
|
const name = `${id}-${placeName}`;
|
||||||
return { name: "places", params: { name } };
|
return { name: "places", params: { name } };
|
||||||
|
@ -147,30 +133,24 @@ export default defineComponent({
|
||||||
|
|
||||||
error() {
|
error() {
|
||||||
return (
|
return (
|
||||||
Boolean(this.data.flag & this.c.FLAG_LOAD_FAIL) ||
|
Boolean(this.data.previewError) ||
|
||||||
Boolean(this.album && this.album.last_added_photo <= 0)
|
Boolean(this.album && this.album.last_added_photo <= 0)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
/**
|
|
||||||
* Open tag event
|
|
||||||
* Unless noNavigate is set, the tag will be opened
|
|
||||||
*/
|
|
||||||
openTag(tag: ITag) {
|
|
||||||
this.$emit("open", tag);
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Mark as loading failed */
|
|
||||||
failed() {
|
failed() {
|
||||||
this.data.flag |= this.c.FLAG_LOAD_FAIL;
|
Vue.set(this.data, "previewError", true);
|
||||||
|
},
|
||||||
|
click() {
|
||||||
|
this.$emit("click", this.data);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.tag,
|
.cluster,
|
||||||
.name,
|
.name,
|
||||||
.bubble,
|
.bubble,
|
||||||
img {
|
img {
|
||||||
|
@ -178,7 +158,7 @@ img {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get rid of color of the bubble
|
// Get rid of color of the bubble
|
||||||
.tag .bbl :deep .counter-bubble__counter {
|
.cluster .bbl :deep .counter-bubble__counter {
|
||||||
color: unset !important;
|
color: unset !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -201,7 +181,7 @@ img {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag.error > & {
|
.cluster.error > & {
|
||||||
color: unset;
|
color: unset;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,15 +6,14 @@
|
||||||
:value.sync="search"
|
:value.sync="search"
|
||||||
:label="t('memories', 'Search')"
|
:label="t('memories', 'Search')"
|
||||||
:placeholder="t('memories', 'Search')"
|
:placeholder="t('memories', 'Search')"
|
||||||
@input="searchChanged"
|
|
||||||
>
|
>
|
||||||
<Magnify :size="16" />
|
<Magnify :size="16" />
|
||||||
</NcTextField>
|
</NcTextField>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="detail">
|
<div v-if="list">
|
||||||
<div class="photo" v-for="photo of detail" :key="photo.fileid">
|
<div class="photo" v-for="photo of filteredList" :key="photo.cluster_id">
|
||||||
<Tag :data="photo" :noNavigate="true" @open="clickFace" />
|
<Cluster :data="photo" :link="false" @click="clickFace" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
|
@ -25,8 +24,8 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from "vue";
|
import { defineComponent } from "vue";
|
||||||
import { IPhoto, ITag } from "../../types";
|
import { ICluster, IFace } from "../../types";
|
||||||
import Tag from "../frame/Tag.vue";
|
import Cluster from "../frame/Cluster.vue";
|
||||||
|
|
||||||
import NcTextField from "@nextcloud/vue/dist/Components/NcTextField";
|
import NcTextField from "@nextcloud/vue/dist/Components/NcTextField";
|
||||||
|
|
||||||
|
@ -38,7 +37,7 @@ import Magnify from "vue-material-design-icons/Magnify.vue";
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "FaceList",
|
name: "FaceList",
|
||||||
components: {
|
components: {
|
||||||
Tag,
|
Cluster,
|
||||||
NcTextField,
|
NcTextField,
|
||||||
Magnify,
|
Magnify,
|
||||||
},
|
},
|
||||||
|
@ -46,9 +45,8 @@ export default defineComponent({
|
||||||
data: () => ({
|
data: () => ({
|
||||||
user: "",
|
user: "",
|
||||||
name: "",
|
name: "",
|
||||||
fullDetail: null as ITag[] | null,
|
list: null as ICluster[] | null,
|
||||||
detail: null as ITag[] | null,
|
fuse: null as Fuse<ICluster>,
|
||||||
fuse: null as Fuse<ITag>,
|
|
||||||
search: "",
|
search: "",
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
@ -62,6 +60,13 @@ export default defineComponent({
|
||||||
this.refreshParams();
|
this.refreshParams();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
filteredList() {
|
||||||
|
if (!this.list || !this.search || !this.fuse) return this.list || [];
|
||||||
|
return this.fuse.search(this.search).map((r) => r.item);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
close() {
|
close() {
|
||||||
this.$emit("close");
|
this.$emit("close");
|
||||||
|
@ -70,43 +75,22 @@ export default defineComponent({
|
||||||
async refreshParams() {
|
async refreshParams() {
|
||||||
this.user = <string>this.$route.params.user || "";
|
this.user = <string>this.$route.params.user || "";
|
||||||
this.name = <string>this.$route.params.name || "";
|
this.name = <string>this.$route.params.name || "";
|
||||||
this.detail = null;
|
this.list = null;
|
||||||
this.fullDetail = null;
|
|
||||||
this.search = "";
|
this.search = "";
|
||||||
|
|
||||||
let data = [];
|
this.list = (await dav.getFaceList(this.$route.name as any)).filter(
|
||||||
let flags = this.c.FLAG_IS_TAG;
|
(c: IFace) => {
|
||||||
if (this.$route.name === "recognize") {
|
const clusterName = String(c.name || c.cluster_id);
|
||||||
data = await dav.getPeopleData("recognize");
|
return c.user_id === this.user && clusterName !== this.name;
|
||||||
flags |= this.c.FLAG_IS_FACE_RECOGNIZE;
|
|
||||||
} else {
|
|
||||||
data = await dav.getPeopleData("facerecognition");
|
|
||||||
flags |= this.c.FLAG_IS_FACE_RECOGNITION;
|
|
||||||
}
|
}
|
||||||
let detail = data[0].detail;
|
);
|
||||||
detail.forEach((photo: IPhoto) => {
|
|
||||||
photo.flag = flags;
|
|
||||||
});
|
|
||||||
detail = detail.filter((photo: ITag) => {
|
|
||||||
const pname = photo.name || photo.fileid.toString();
|
|
||||||
return photo.user_id === this.user && pname !== this.name;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.detail = detail;
|
this.fuse = new Fuse(this.list, { keys: ["name"] });
|
||||||
this.fullDetail = detail;
|
|
||||||
this.fuse = new Fuse(detail, { keys: ["name"] });
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async clickFace(face: ITag) {
|
async clickFace(face: IFace) {
|
||||||
this.$emit("select", face);
|
this.$emit("select", face);
|
||||||
},
|
},
|
||||||
|
|
||||||
searchChanged() {
|
|
||||||
if (!this.detail) return;
|
|
||||||
this.detail = this.search
|
|
||||||
? this.fuse.search(this.search).map((r) => r.item)
|
|
||||||
: this.fullDetail;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -35,8 +35,8 @@ const NcProgressBar = () =>
|
||||||
|
|
||||||
import { showError } from "@nextcloud/dialogs";
|
import { showError } from "@nextcloud/dialogs";
|
||||||
import { getCurrentUser } from "@nextcloud/auth";
|
import { getCurrentUser } from "@nextcloud/auth";
|
||||||
import { IFileInfo, ITag } from "../../types";
|
import { IFileInfo, IFace } from "../../types";
|
||||||
import Tag from "../frame/Tag.vue";
|
import Cluster from "../frame/Cluster.vue";
|
||||||
import FaceList from "./FaceList.vue";
|
import FaceList from "./FaceList.vue";
|
||||||
|
|
||||||
import Modal from "./Modal.vue";
|
import Modal from "./Modal.vue";
|
||||||
|
@ -50,7 +50,7 @@ export default defineComponent({
|
||||||
NcTextField,
|
NcTextField,
|
||||||
NcProgressBar,
|
NcProgressBar,
|
||||||
Modal,
|
Modal,
|
||||||
Tag,
|
Cluster,
|
||||||
FaceList,
|
FaceList,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -79,11 +79,11 @@ export default defineComponent({
|
||||||
this.show = true;
|
this.show = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
async clickFace(face: ITag) {
|
async clickFace(face: IFace) {
|
||||||
const user = this.$route.params.user || "";
|
const user = this.$route.params.user || "";
|
||||||
const name = this.$route.params.name || "";
|
const name = this.$route.params.name || "";
|
||||||
|
|
||||||
const newName = face.name || face.fileid.toString();
|
const newName = String(face.name || face.cluster_id);
|
||||||
if (
|
if (
|
||||||
!confirm(
|
!confirm(
|
||||||
this.t(
|
this.t(
|
||||||
|
|
|
@ -24,8 +24,8 @@ const NcTextField = () => import("@nextcloud/vue/dist/Components/NcTextField");
|
||||||
|
|
||||||
import { showError } from "@nextcloud/dialogs";
|
import { showError } from "@nextcloud/dialogs";
|
||||||
import { getCurrentUser } from "@nextcloud/auth";
|
import { getCurrentUser } from "@nextcloud/auth";
|
||||||
import { IPhoto, ITag } from "../../types";
|
import { IPhoto, IFace } from "../../types";
|
||||||
import Tag from "../frame/Tag.vue";
|
import Cluster from "../frame/Cluster.vue";
|
||||||
import FaceList from "./FaceList.vue";
|
import FaceList from "./FaceList.vue";
|
||||||
|
|
||||||
import Modal from "./Modal.vue";
|
import Modal from "./Modal.vue";
|
||||||
|
@ -38,7 +38,7 @@ export default defineComponent({
|
||||||
NcButton,
|
NcButton,
|
||||||
NcTextField,
|
NcTextField,
|
||||||
Modal,
|
Modal,
|
||||||
Tag,
|
Cluster,
|
||||||
FaceList,
|
FaceList,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -86,11 +86,11 @@ export default defineComponent({
|
||||||
this.$emit("moved", list);
|
this.$emit("moved", list);
|
||||||
},
|
},
|
||||||
|
|
||||||
async clickFace(face: ITag) {
|
async clickFace(face: IFace) {
|
||||||
const user = this.$route.params.user || "";
|
const user = this.$route.params.user || "";
|
||||||
const name = this.$route.params.name || "";
|
const name = this.$route.params.name || "";
|
||||||
|
|
||||||
const newName = face.name || face.fileid.toString();
|
const newName = String(face.name || face.cluster_id);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!confirm(
|
!confirm(
|
||||||
|
|
|
@ -206,26 +206,3 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.top-matter {
|
|
||||||
display: flex;
|
|
||||||
vertical-align: middle;
|
|
||||||
|
|
||||||
.name {
|
|
||||||
font-size: 1.3em;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 40px;
|
|
||||||
padding-left: 3px;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.right-actions {
|
|
||||||
margin-right: 40px;
|
|
||||||
z-index: 50;
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="name" class="tag-top-matter">
|
<div class="top-matter">
|
||||||
<NcActions>
|
<NcActions v-if="name">
|
||||||
<NcActionButton :aria-label="t('memories', 'Back')" @click="back()">
|
<NcActionButton :aria-label="t('memories', 'Back')" @click="back()">
|
||||||
{{ t("memories", "Back") }}
|
{{ t("memories", "Back") }}
|
||||||
<template #icon> <BackIcon :size="20" /> </template>
|
<template #icon> <BackIcon :size="20" /> </template>
|
||||||
</NcActionButton>
|
</NcActionButton>
|
||||||
</NcActions>
|
</NcActions>
|
||||||
<span class="name">{{ name }}</span>
|
<span class="name">{{ name || viewname }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ import { defineComponent } from "vue";
|
||||||
|
|
||||||
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";
|
||||||
|
import * as strings from "../../services/strings";
|
||||||
|
|
||||||
import BackIcon from "vue-material-design-icons/ArrowLeft.vue";
|
import BackIcon from "vue-material-design-icons/ArrowLeft.vue";
|
||||||
|
|
||||||
|
@ -26,48 +27,27 @@ export default defineComponent({
|
||||||
BackIcon,
|
BackIcon,
|
||||||
},
|
},
|
||||||
|
|
||||||
data: () => ({
|
computed: {
|
||||||
name: "",
|
viewname(): string {
|
||||||
}),
|
return strings.viewName(this.$route.name);
|
||||||
|
|
||||||
watch: {
|
|
||||||
$route: function (from: any, to: any) {
|
|
||||||
this.createMatter();
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
name(): string | null {
|
||||||
this.createMatter();
|
switch (this.$route.name) {
|
||||||
|
case "tags":
|
||||||
|
return this.$route.params.name;
|
||||||
|
case "places":
|
||||||
|
return this.$route.params.name?.split("-").slice(1).join("-");
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
createMatter() {
|
|
||||||
this.name = <string>this.$route.params.name || "";
|
|
||||||
|
|
||||||
if (this.$route.name === "places") {
|
|
||||||
this.name = this.name.split("-").slice(1).join("-");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
back() {
|
back() {
|
||||||
this.$router.push({ name: this.$route.name });
|
this.$router.push({ name: this.$route.name });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.tag-top-matter {
|
|
||||||
.name {
|
|
||||||
font-size: 1.3em;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 42px;
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
<template>
|
||||||
|
<NcEmptyContent
|
||||||
|
:title="t('memories', 'Nothing to show here')"
|
||||||
|
:description="emptyViewDescription"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<PeopleIcon v-if="routeIsPeople" />
|
||||||
|
<ArchiveIcon v-else-if="routeIsArchive" />
|
||||||
|
<ImageMultipleIcon v-else />
|
||||||
|
</template>
|
||||||
|
</NcEmptyContent>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
|
||||||
|
import NcEmptyContent from "@nextcloud/vue/dist/Components/NcEmptyContent";
|
||||||
|
|
||||||
|
import PeopleIcon from "vue-material-design-icons/AccountMultiple.vue";
|
||||||
|
import ImageMultipleIcon from "vue-material-design-icons/ImageMultiple.vue";
|
||||||
|
import ArchiveIcon from "vue-material-design-icons/PackageDown.vue";
|
||||||
|
|
||||||
|
import * as strings from "../../services/strings";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "EmptyContent",
|
||||||
|
|
||||||
|
components: {
|
||||||
|
NcEmptyContent,
|
||||||
|
|
||||||
|
PeopleIcon,
|
||||||
|
ArchiveIcon,
|
||||||
|
ImageMultipleIcon,
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
emptyViewDescription(): string {
|
||||||
|
return strings.emptyDescription(this.$route.name);
|
||||||
|
},
|
||||||
|
|
||||||
|
routeIsPeople(): boolean {
|
||||||
|
return (
|
||||||
|
this.$route.name === "recognize" ||
|
||||||
|
this.$route.name === "facerecognition"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
routeIsArchive(): boolean {
|
||||||
|
return this.$route.name === "archive";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -119,26 +119,3 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.face-top-matter {
|
|
||||||
display: flex;
|
|
||||||
vertical-align: middle;
|
|
||||||
|
|
||||||
.name {
|
|
||||||
font-size: 1.3em;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 40px;
|
|
||||||
padding-left: 3px;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.right-actions {
|
|
||||||
margin-right: 40px;
|
|
||||||
z-index: 50;
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -131,23 +131,8 @@ export default defineComponent({
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.top-matter {
|
.top-matter {
|
||||||
display: flex;
|
|
||||||
vertical-align: middle;
|
|
||||||
|
|
||||||
.breadcrumb {
|
.breadcrumb {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-actions {
|
|
||||||
margin-right: 40px;
|
|
||||||
z-index: 50;
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep span {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="top-matter" v-if="type">
|
<div class="top-matter-container" v-if="type">
|
||||||
<FolderTopMatter v-if="type === 1" />
|
<FolderTopMatter v-if="type === 1" />
|
||||||
<TagTopMatter v-else-if="type === 2" />
|
<ClusterTopMatter v-else-if="type === 2" />
|
||||||
<FaceTopMatter v-else-if="type === 3" />
|
<FaceTopMatter v-else-if="type === 3" />
|
||||||
<AlbumTopMatter v-else-if="type === 4" />
|
<AlbumTopMatter v-else-if="type === 4" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -11,7 +11,7 @@
|
||||||
import { defineComponent } from "vue";
|
import { defineComponent } from "vue";
|
||||||
|
|
||||||
import FolderTopMatter from "./FolderTopMatter.vue";
|
import FolderTopMatter from "./FolderTopMatter.vue";
|
||||||
import TagTopMatter from "./TagTopMatter.vue";
|
import ClusterTopMatter from "./ClusterTopMatter.vue";
|
||||||
import FaceTopMatter from "./FaceTopMatter.vue";
|
import FaceTopMatter from "./FaceTopMatter.vue";
|
||||||
import AlbumTopMatter from "./AlbumTopMatter.vue";
|
import AlbumTopMatter from "./AlbumTopMatter.vue";
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ export default defineComponent({
|
||||||
name: "TopMatter",
|
name: "TopMatter",
|
||||||
components: {
|
components: {
|
||||||
FolderTopMatter,
|
FolderTopMatter,
|
||||||
TagTopMatter,
|
ClusterTopMatter,
|
||||||
FaceTopMatter,
|
FaceTopMatter,
|
||||||
AlbumTopMatter,
|
AlbumTopMatter,
|
||||||
},
|
},
|
||||||
|
@ -47,21 +47,16 @@ export default defineComponent({
|
||||||
switch (this.$route.name) {
|
switch (this.$route.name) {
|
||||||
case "folders":
|
case "folders":
|
||||||
return TopMatterType.FOLDER;
|
return TopMatterType.FOLDER;
|
||||||
|
case "albums":
|
||||||
|
return TopMatterType.ALBUM;
|
||||||
case "tags":
|
case "tags":
|
||||||
return this.$route.params.name
|
case "places":
|
||||||
? TopMatterType.TAG
|
return TopMatterType.CLUSTER;
|
||||||
: TopMatterType.NONE;
|
|
||||||
case "recognize":
|
case "recognize":
|
||||||
case "facerecognition":
|
case "facerecognition":
|
||||||
return this.$route.params.name
|
return this.$route.params.name
|
||||||
? TopMatterType.FACE
|
? TopMatterType.FACE
|
||||||
: TopMatterType.NONE;
|
: TopMatterType.CLUSTER;
|
||||||
case "albums":
|
|
||||||
return TopMatterType.ALBUM;
|
|
||||||
case "places":
|
|
||||||
return this.$route.params.name
|
|
||||||
? TopMatterType.TAG
|
|
||||||
: TopMatterType.NONE;
|
|
||||||
default:
|
default:
|
||||||
return TopMatterType.NONE;
|
return TopMatterType.NONE;
|
||||||
}
|
}
|
||||||
|
@ -70,3 +65,46 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.top-matter-container {
|
||||||
|
padding-top: 4px;
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
padding-left: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div {
|
||||||
|
display: flex;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep .name {
|
||||||
|
font-size: 1.3em;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 42px;
|
||||||
|
vertical-align: top;
|
||||||
|
flex-grow: 1;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep button + .name {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep .right-actions {
|
||||||
|
margin-right: 40px;
|
||||||
|
z-index: 50;
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep button {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -94,13 +94,21 @@ body.has-viewer header {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide scrollbar
|
// Hide scrollbar
|
||||||
.recycler::-webkit-scrollbar {
|
@mixin hide-scrollbar {
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
&::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
width: 0 !important;
|
width: 0 !important;
|
||||||
}
|
}
|
||||||
.recycler {
|
}
|
||||||
scrollbar-width: none;
|
.hide-scrollbar {
|
||||||
-ms-overflow-style: none;
|
@include hide-scrollbar;
|
||||||
|
}
|
||||||
|
.hide-scrollbar-mobile {
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
@include hide-scrollbar;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make metadata tab scrollbar thin
|
// Make metadata tab scrollbar thin
|
||||||
|
|
|
@ -4,6 +4,7 @@ import Router from "vue-router";
|
||||||
import Vue from "vue";
|
import Vue from "vue";
|
||||||
import Timeline from "./components/Timeline.vue";
|
import Timeline from "./components/Timeline.vue";
|
||||||
import SplitTimeline from "./components/SplitTimeline.vue";
|
import SplitTimeline from "./components/SplitTimeline.vue";
|
||||||
|
import ClusterView from "./components/ClusterView.vue";
|
||||||
|
|
||||||
Vue.use(Router);
|
Vue.use(Router);
|
||||||
|
|
||||||
|
@ -52,7 +53,7 @@ export default new Router({
|
||||||
|
|
||||||
{
|
{
|
||||||
path: "/albums/:user?/:name?",
|
path: "/albums/:user?/:name?",
|
||||||
component: Timeline,
|
component: ClusterView,
|
||||||
name: "albums",
|
name: "albums",
|
||||||
props: (route) => ({
|
props: (route) => ({
|
||||||
rootTitle: t("memories", "Albums"),
|
rootTitle: t("memories", "Albums"),
|
||||||
|
@ -79,7 +80,7 @@ export default new Router({
|
||||||
|
|
||||||
{
|
{
|
||||||
path: "/recognize/:user?/:name?",
|
path: "/recognize/:user?/:name?",
|
||||||
component: Timeline,
|
component: ClusterView,
|
||||||
name: "recognize",
|
name: "recognize",
|
||||||
props: (route) => ({
|
props: (route) => ({
|
||||||
rootTitle: t("memories", "People"),
|
rootTitle: t("memories", "People"),
|
||||||
|
@ -88,7 +89,7 @@ export default new Router({
|
||||||
|
|
||||||
{
|
{
|
||||||
path: "/facerecognition/:user?/:name?",
|
path: "/facerecognition/:user?/:name?",
|
||||||
component: Timeline,
|
component: ClusterView,
|
||||||
name: "facerecognition",
|
name: "facerecognition",
|
||||||
props: (route) => ({
|
props: (route) => ({
|
||||||
rootTitle: t("memories", "People"),
|
rootTitle: t("memories", "People"),
|
||||||
|
@ -97,7 +98,7 @@ export default new Router({
|
||||||
|
|
||||||
{
|
{
|
||||||
path: "/places/:name*",
|
path: "/places/:name*",
|
||||||
component: Timeline,
|
component: ClusterView,
|
||||||
name: "places",
|
name: "places",
|
||||||
props: (route) => ({
|
props: (route) => ({
|
||||||
rootTitle: t("memories", "Places"),
|
rootTitle: t("memories", "Places"),
|
||||||
|
@ -106,7 +107,7 @@ export default new Router({
|
||||||
|
|
||||||
{
|
{
|
||||||
path: "/tags/:name*",
|
path: "/tags/:name*",
|
||||||
component: Timeline,
|
component: ClusterView,
|
||||||
name: "tags",
|
name: "tags",
|
||||||
props: (route) => ({
|
props: (route) => ({
|
||||||
rootTitle: t("memories", "Tags"),
|
rootTitle: t("memories", "Tags"),
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { generateUrl } from "@nextcloud/router";
|
import { generateUrl } from "@nextcloud/router";
|
||||||
|
import { ClusterTypes } from "../types";
|
||||||
|
|
||||||
const BASE = "/apps/memories/api";
|
const BASE = "/apps/memories/api";
|
||||||
|
|
||||||
|
@ -65,7 +66,7 @@ export class API {
|
||||||
return tok(gen(`${BASE}/days/{id}`, { id }));
|
return tok(gen(`${BASE}/days/{id}`, { id }));
|
||||||
}
|
}
|
||||||
|
|
||||||
static DAYS_FILTER(query: any, filter: DaysFilterType, value: string = '1') {
|
static DAYS_FILTER(query: any, filter: DaysFilterType, value: string = "1") {
|
||||||
query[filter] = value;
|
query[filter] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,25 +75,20 @@ export class API {
|
||||||
}
|
}
|
||||||
|
|
||||||
static ALBUM_DOWNLOAD(user: string, name: string) {
|
static ALBUM_DOWNLOAD(user: string, name: string) {
|
||||||
return gen(`${BASE}/clusters/albums/download?name={user}/{name}`, { user, name });
|
return gen(`${BASE}/clusters/albums/download?name={user}/{name}`, {
|
||||||
|
user,
|
||||||
|
name,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static PLACE_LIST() {
|
static PLACE_LIST() {
|
||||||
return gen(`${BASE}/clusters/places`);
|
return gen(`${BASE}/clusters/places`);
|
||||||
}
|
}
|
||||||
|
|
||||||
static PLACE_PREVIEW(place: number | string) {
|
|
||||||
return gen(`${BASE}/clusters/places/preview/{place}`, { place });
|
|
||||||
}
|
|
||||||
|
|
||||||
static TAG_LIST() {
|
static TAG_LIST() {
|
||||||
return gen(`${BASE}/clusters/tags`);
|
return gen(`${BASE}/clusters/tags`);
|
||||||
}
|
}
|
||||||
|
|
||||||
static TAG_PREVIEW(tag: string) {
|
|
||||||
return gen(`${BASE}/clusters/tags/preview/{tag}`, { tag });
|
|
||||||
}
|
|
||||||
|
|
||||||
static TAG_SET(fileid: string | number) {
|
static TAG_SET(fileid: string | number) {
|
||||||
return gen(`${BASE}/tags/set/{fileid}`, { fileid });
|
return gen(`${BASE}/tags/set/{fileid}`, { fileid });
|
||||||
}
|
}
|
||||||
|
@ -101,11 +97,8 @@ export class API {
|
||||||
return gen(`${BASE}/clusters/${app}`);
|
return gen(`${BASE}/clusters/${app}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
static FACE_PREVIEW(
|
static CLUSTER_PREVIEW(backend: ClusterTypes, name: string | number) {
|
||||||
app: "recognize" | "facerecognition",
|
return gen(`${BASE}/clusters/${backend}/preview/{name}`, { name });
|
||||||
face: string | number
|
|
||||||
) {
|
|
||||||
return gen(`${BASE}/clusters/${app}/preview/{face}`, { face });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static ARCHIVE(fileid: number) {
|
static ARCHIVE(fileid: number) {
|
||||||
|
|
|
@ -212,27 +212,6 @@ export function convertFlags(photo: IPhoto) {
|
||||||
photo.flag |= constants.c.FLAG_IS_FOLDER;
|
photo.flag |= constants.c.FLAG_IS_FOLDER;
|
||||||
delete photo.isfolder;
|
delete photo.isfolder;
|
||||||
}
|
}
|
||||||
if (photo.isface) {
|
|
||||||
const app = photo.isface;
|
|
||||||
if (app === "recognize") {
|
|
||||||
photo.flag |= constants.c.FLAG_IS_FACE_RECOGNIZE;
|
|
||||||
} else if (app === "facerecognition") {
|
|
||||||
photo.flag |= constants.c.FLAG_IS_FACE_RECOGNITION;
|
|
||||||
}
|
|
||||||
delete photo.isface;
|
|
||||||
}
|
|
||||||
if (photo.isplace) {
|
|
||||||
photo.flag |= constants.c.FLAG_IS_PLACE;
|
|
||||||
delete photo.isplace;
|
|
||||||
}
|
|
||||||
if (photo.istag) {
|
|
||||||
photo.flag |= constants.c.FLAG_IS_TAG;
|
|
||||||
delete photo.istag;
|
|
||||||
}
|
|
||||||
if (photo.isalbum) {
|
|
||||||
photo.flag |= constants.c.FLAG_IS_ALBUM;
|
|
||||||
delete photo.isalbum;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -325,13 +304,8 @@ export const constants = {
|
||||||
FLAG_IS_VIDEO: 1 << 2,
|
FLAG_IS_VIDEO: 1 << 2,
|
||||||
FLAG_IS_FAVORITE: 1 << 3,
|
FLAG_IS_FAVORITE: 1 << 3,
|
||||||
FLAG_IS_FOLDER: 1 << 4,
|
FLAG_IS_FOLDER: 1 << 4,
|
||||||
FLAG_IS_ALBUM: 1 << 5,
|
FLAG_SELECTED: 1 << 5,
|
||||||
FLAG_IS_FACE_RECOGNIZE: 1 << 6,
|
FLAG_LEAVING: 1 << 6,
|
||||||
FLAG_IS_FACE_RECOGNITION: 1 << 7,
|
|
||||||
FLAG_IS_PLACE: 1 << 8,
|
|
||||||
FLAG_IS_TAG: 1 << 9,
|
|
||||||
FLAG_SELECTED: 1 << 10,
|
|
||||||
FLAG_LEAVING: 1 << 11,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
TagDayID: TagDayID,
|
TagDayID: TagDayID,
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import * as base from "./base";
|
import * as base from "./base";
|
||||||
import { getCurrentUser } from "@nextcloud/auth";
|
import { getCurrentUser } from "@nextcloud/auth";
|
||||||
import { showError } from "@nextcloud/dialogs";
|
import { showError } from "@nextcloud/dialogs";
|
||||||
import { translate as t, translatePlural as n } from "@nextcloud/l10n";
|
import { translate as t } from "@nextcloud/l10n";
|
||||||
import { IAlbum, IDay, IFileInfo, IPhoto, ITag } from "../../types";
|
import { IAlbum, IFileInfo, IPhoto } from "../../types";
|
||||||
import { constants } from "../Utils";
|
|
||||||
import { API } from "../API";
|
import { API } from "../API";
|
||||||
import axios from "@nextcloud/axios";
|
import axios from "@nextcloud/axios";
|
||||||
import client from "../DavClient";
|
import client from "../DavClient";
|
||||||
|
@ -22,21 +21,12 @@ export function getAlbumPath(user: string, name: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get list of albums and convert to Days response
|
* Get list of albums.
|
||||||
* @param type Type of albums to get; 1 = personal, 2 = shared, 3 = all
|
* @param type Type of albums to get; 1 = personal, 2 = shared, 3 = all
|
||||||
* @param sortOrder Sort order; 1 = by date, 2 = by name
|
* @param sortOrder Sort order; 1 = by date, 2 = by name
|
||||||
*/
|
*/
|
||||||
export async function getAlbumsData(
|
export async function getAlbums(type: 1 | 2 | 3, sortOrder: 1 | 2) {
|
||||||
type: 1 | 2 | 3,
|
const data = (await axios.get<IAlbum[]>(API.ALBUM_LIST(type))).data;
|
||||||
sortOrder: 1 | 2
|
|
||||||
): Promise<IDay[]> {
|
|
||||||
let data: IAlbum[] = [];
|
|
||||||
try {
|
|
||||||
const res = await axios.get<typeof data>(API.ALBUM_LIST(type));
|
|
||||||
data = res.data;
|
|
||||||
} catch (e) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Response is already sorted by date, sort otherwise
|
// Response is already sorted by date, sort otherwise
|
||||||
if (sortOrder === 2) {
|
if (sortOrder === 2) {
|
||||||
|
@ -45,23 +35,7 @@ export async function getAlbumsData(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to days response
|
return data;
|
||||||
return [
|
|
||||||
{
|
|
||||||
dayid: constants.TagDayID.ALBUMS,
|
|
||||||
count: data.length,
|
|
||||||
detail: data.map(
|
|
||||||
(album) =>
|
|
||||||
({
|
|
||||||
...album,
|
|
||||||
fileid: album.album_id,
|
|
||||||
flag: constants.c.FLAG_IS_TAG & constants.c.FLAG_IS_ALBUM,
|
|
||||||
istag: true,
|
|
||||||
isalbum: true,
|
|
||||||
} as ITag)
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -2,47 +2,13 @@ import axios from "@nextcloud/axios";
|
||||||
import { showError } from "@nextcloud/dialogs";
|
import { showError } from "@nextcloud/dialogs";
|
||||||
import { translate as t } from "@nextcloud/l10n";
|
import { translate as t } from "@nextcloud/l10n";
|
||||||
import { generateUrl } from "@nextcloud/router";
|
import { generateUrl } from "@nextcloud/router";
|
||||||
import { IDay, IPhoto } from "../../types";
|
import { IFace, IPhoto } from "../../types";
|
||||||
import { API } from "../API";
|
import { API } from "../API";
|
||||||
import { constants } from "../Utils";
|
|
||||||
import client from "../DavClient";
|
import client from "../DavClient";
|
||||||
import * as base from "./base";
|
import * as base from "./base";
|
||||||
|
|
||||||
/**
|
export async function getFaceList(app: "recognize" | "facerecognition") {
|
||||||
* Get list of tags and convert to Days response
|
return (await axios.get<IFace[]>(API.FACE_LIST(app))).data;
|
||||||
*/
|
|
||||||
export async function getPeopleData(
|
|
||||||
app: "recognize" | "facerecognition"
|
|
||||||
): Promise<IDay[]> {
|
|
||||||
// Query for photos
|
|
||||||
let data: {
|
|
||||||
id: number;
|
|
||||||
count: number;
|
|
||||||
name: string;
|
|
||||||
}[] = [];
|
|
||||||
try {
|
|
||||||
const res = await axios.get<typeof data>(API.FACE_LIST(app));
|
|
||||||
data = res.data;
|
|
||||||
} catch (e) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to days response
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
dayid: constants.TagDayID.FACES,
|
|
||||||
count: data.length,
|
|
||||||
detail: data.map(
|
|
||||||
(face) =>
|
|
||||||
({
|
|
||||||
...face,
|
|
||||||
fileid: face.id,
|
|
||||||
istag: true,
|
|
||||||
isface: app,
|
|
||||||
} as any)
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updatePeopleFaceRecognition(
|
export async function updatePeopleFaceRecognition(
|
||||||
|
|
|
@ -1,41 +1,7 @@
|
||||||
import { IDay, IPhoto, ITag } from "../../types";
|
import { ICluster } from "../../types";
|
||||||
import { constants } from "../Utils";
|
|
||||||
import { API } from "../API";
|
import { API } from "../API";
|
||||||
import axios from "@nextcloud/axios";
|
import axios from "@nextcloud/axios";
|
||||||
|
|
||||||
/**
|
export async function getPlaces() {
|
||||||
* Get list of tags and convert to Days response
|
return (await axios.get<ICluster[]>(API.PLACE_LIST())).data;
|
||||||
*/
|
|
||||||
export async function getPlacesData(): Promise<IDay[]> {
|
|
||||||
// Query for photos
|
|
||||||
let data: {
|
|
||||||
osm_id: number;
|
|
||||||
count: number;
|
|
||||||
name: string;
|
|
||||||
}[] = [];
|
|
||||||
try {
|
|
||||||
const res = await axios.get<typeof data>(API.PLACE_LIST());
|
|
||||||
data = res.data;
|
|
||||||
} catch (e) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to days response
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
dayid: constants.TagDayID.TAGS,
|
|
||||||
count: data.length,
|
|
||||||
detail: data.map(
|
|
||||||
(tag) =>
|
|
||||||
({
|
|
||||||
...tag,
|
|
||||||
id: tag.osm_id,
|
|
||||||
fileid: tag.osm_id,
|
|
||||||
flag: constants.c.FLAG_IS_TAG,
|
|
||||||
istag: true,
|
|
||||||
isplace: true,
|
|
||||||
} as ITag)
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,39 +1,10 @@
|
||||||
import { IDay, IPhoto, ITag } from "../../types";
|
import { ICluster } from "../../types";
|
||||||
import { constants, hashCode } from "../Utils";
|
|
||||||
import { API } from "../API";
|
import { API } from "../API";
|
||||||
import axios from "@nextcloud/axios";
|
import axios from "@nextcloud/axios";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get list of tags and convert to Days response
|
* Get list of tags.
|
||||||
*/
|
*/
|
||||||
export async function getTagsData(): Promise<IDay[]> {
|
export async function getTags() {
|
||||||
// Query for photos
|
return (await axios.get<ICluster[]>(API.TAG_LIST())).data;
|
||||||
let data: {
|
|
||||||
id: number;
|
|
||||||
count: number;
|
|
||||||
name: string;
|
|
||||||
}[] = [];
|
|
||||||
try {
|
|
||||||
const res = await axios.get<typeof data>(API.TAG_LIST());
|
|
||||||
data = res.data;
|
|
||||||
} catch (e) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to days response
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
dayid: constants.TagDayID.TAGS,
|
|
||||||
count: data.length,
|
|
||||||
detail: data.map(
|
|
||||||
(tag) =>
|
|
||||||
({
|
|
||||||
...tag,
|
|
||||||
fileid: hashCode(tag.name),
|
|
||||||
flag: constants.c.FLAG_IS_TAG,
|
|
||||||
istag: true,
|
|
||||||
} as ITag)
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
import { loadState } from "@nextcloud/initial-state";
|
||||||
|
import { translate as t } from "@nextcloud/l10n";
|
||||||
|
|
||||||
|
const config_facerecognitionEnabled = Boolean(
|
||||||
|
loadState("memories", "facerecognitionEnabled", <string>"")
|
||||||
|
);
|
||||||
|
|
||||||
|
export function emptyDescription(routeName: string): string {
|
||||||
|
switch (routeName) {
|
||||||
|
case "timeline":
|
||||||
|
return t(
|
||||||
|
"memories",
|
||||||
|
"Upload some photos and make sure the timeline path is configured"
|
||||||
|
);
|
||||||
|
case "favorites":
|
||||||
|
return t("memories", "Mark photos as favorite to find them easily");
|
||||||
|
case "thisday":
|
||||||
|
return t("memories", "Memories from past years will appear here");
|
||||||
|
case "facerecognition":
|
||||||
|
return config_facerecognitionEnabled
|
||||||
|
? t("memories", "You will find your friends soon. Please be patient")
|
||||||
|
: t(
|
||||||
|
"memories",
|
||||||
|
"Face Recognition is disabled. Enable in settings to find your friends"
|
||||||
|
);
|
||||||
|
case "videos":
|
||||||
|
return t("memories", "Your videos will appear here");
|
||||||
|
case "albums":
|
||||||
|
return t("memories", "Create an album to get started");
|
||||||
|
case "archive":
|
||||||
|
return t(
|
||||||
|
"memories",
|
||||||
|
"Archive photos you don't want to see in your timeline"
|
||||||
|
);
|
||||||
|
case "tags":
|
||||||
|
return t("memories", "Tag photos to find them easily");
|
||||||
|
case "recognize":
|
||||||
|
return t("memories", "Recognize is still working on your photos");
|
||||||
|
case "places":
|
||||||
|
return t("memories", "Places you have been to will appear here");
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function viewName(routeName: string): string {
|
||||||
|
switch (routeName) {
|
||||||
|
case "timeline":
|
||||||
|
return t("memories", "Your Timeline");
|
||||||
|
case "favorites":
|
||||||
|
return t("memories", "Favorites");
|
||||||
|
case "recognize":
|
||||||
|
case "facerecognition":
|
||||||
|
return t("memories", "People");
|
||||||
|
case "videos":
|
||||||
|
return t("memories", "Videos");
|
||||||
|
case "albums":
|
||||||
|
return t("memories", "Albums");
|
||||||
|
case "archive":
|
||||||
|
return t("memories", "Archive");
|
||||||
|
case "thisday":
|
||||||
|
return t("memories", "On this day");
|
||||||
|
case "tags":
|
||||||
|
return t("memories", "Tags");
|
||||||
|
case "places":
|
||||||
|
return t("memories", "Places");
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
35
src/types.ts
35
src/types.ts
|
@ -117,18 +117,28 @@ export interface IFolder extends IPhoto {
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ITag extends IPhoto {
|
export type ClusterTypes =
|
||||||
/** Name of tag */
|
| "tags"
|
||||||
name: string;
|
| "albums"
|
||||||
/** Number of images in this tag */
|
| "places"
|
||||||
|
| "recognize"
|
||||||
|
| "facerecognition";
|
||||||
|
|
||||||
|
export interface ICluster {
|
||||||
|
/** A unique identifier for the cluster */
|
||||||
|
cluster_id: number | string;
|
||||||
|
/** Type of cluster */
|
||||||
|
cluster_type: ClusterTypes;
|
||||||
|
/** Number of images in this cluster */
|
||||||
count: number;
|
count: number;
|
||||||
/** User for face if face */
|
/** Name of cluster */
|
||||||
user_id?: string;
|
name: string;
|
||||||
/** Cache of previews */
|
|
||||||
previews?: IPhoto[];
|
/** Preview loading failed */
|
||||||
|
previewError?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAlbum extends ITag {
|
export interface IAlbum extends ICluster {
|
||||||
/** ID of album */
|
/** ID of album */
|
||||||
album_id: number;
|
album_id: number;
|
||||||
/** Owner of album */
|
/** Owner of album */
|
||||||
|
@ -141,6 +151,11 @@ export interface IAlbum extends ITag {
|
||||||
last_added_photo: number;
|
last_added_photo: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IFace extends ICluster {
|
||||||
|
/** User for face */
|
||||||
|
user_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IFaceRect {
|
export interface IFaceRect {
|
||||||
w: number;
|
w: number;
|
||||||
h: number;
|
h: number;
|
||||||
|
@ -211,7 +226,7 @@ export type TopMatter = {
|
||||||
export enum TopMatterType {
|
export enum TopMatterType {
|
||||||
NONE = 0,
|
NONE = 0,
|
||||||
FOLDER = 1,
|
FOLDER = 1,
|
||||||
TAG = 2,
|
CLUSTER = 2,
|
||||||
FACE = 3,
|
FACE = 3,
|
||||||
ALBUM = 4,
|
ALBUM = 4,
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue