refactor: tag frame to cluster

Signed-off-by: Varun Patil <varunpatil@ucla.edu>
pull/563/head
Varun Patil 2023-03-24 12:30:08 -07:00
parent 92781df0c1
commit 7326ee8ec0
23 changed files with 480 additions and 509 deletions

View File

@ -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>

View File

@ -528,8 +528,7 @@ export default defineComponent({
selectPhoto(photo: IPhoto, val?: boolean, noUpdate?: boolean) {
if (
photo.flag & this.c.FLAG_PLACEHOLDER ||
photo.flag & this.c.FLAG_IS_FOLDER ||
photo.flag & this.c.FLAG_IS_TAG
photo.flag & this.c.FLAG_IS_FOLDER
) {
return; // ignore placeholders
}

View File

@ -8,22 +8,12 @@
<TopMatter ref="topmatter" />
<!-- No content found and nothing is loading -->
<NcEmptyContent
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>
<EmptyContent v-if="loading === 0 && list.length === 0" />
<!-- Main recycler view for rows -->
<RecycleScroller
ref="recycler"
class="recycler"
class="recycler hide-scrollbar"
:class="{ empty: list.length === 0 }"
:items="list"
:emit-update="true"
@ -83,8 +73,6 @@
>
<Folder v-if="photo.flag & c.FLAG_IS_FOLDER" :data="photo" />
<Tag v-else-if="photo.flag & c.FLAG_IS_TAG" :data="photo" />
<Photo
v-else
:data="photo"
@ -137,7 +125,6 @@ import { defineComponent } from "vue";
import axios from "@nextcloud/axios";
import { showError } from "@nextcloud/dialogs";
import { subscribe, unsubscribe } from "@nextcloud/event-bus";
import NcEmptyContent from "@nextcloud/vue/dist/Components/NcEmptyContent";
import { getLayout } from "../services/Layout";
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 Folder from "./frame/Folder.vue";
import Photo from "./frame/Photo.vue";
import Tag from "./frame/Tag.vue";
import ScrollerManager from "./ScrollerManager.vue";
import SelectionManager from "./SelectionManager.vue";
import Viewer from "./viewer/Viewer.vue";
import EmptyContent from "./top-matter/EmptyContent.vue";
import OnThisDay from "./top-matter/OnThisDay.vue";
import TopMatter from "./top-matter/TopMatter.vue";
import * as dav from "../services/DavRequests";
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 ImageMultipleIcon from "vue-material-design-icons/ImageMultiple.vue";
import ArchiveIcon from "vue-material-design-icons/PackageDown.vue";
import { API, DaysFilterType } from "../services/API";
const SCROLL_LOAD_DELAY = 100; // Delay in loading data when scrolling
@ -170,19 +156,15 @@ export default defineComponent({
components: {
Folder,
Tag,
Photo,
TopMatter,
EmptyContent,
OnThisDay,
TopMatter,
SelectionManager,
ScrollerManager,
Viewer,
NcEmptyContent,
CheckCircle,
ArchiveIcon,
PeopleIcon,
ImageMultipleIcon,
},
mixins: [UserConfig],
@ -279,54 +261,7 @@ export default defineComponent({
},
/** Get view name for dynamic top matter */
viewName(): string {
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");
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 "";
}
return strings.viewName(this.$route.name);
},
},
@ -681,7 +616,11 @@ export default defineComponent({
// Map Bounds
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
@ -739,14 +678,6 @@ export default defineComponent({
let data: IDay[] = [];
if (this.$route.name === "thisday") {
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()) {
data = await dav.getSingleItemData();
this.$router.replace(utils.getViewerRoute(data[0]!.detail[0]));
@ -1043,9 +974,7 @@ export default defineComponent({
return {
width: p.w || this.rowHeight,
height: p.h || this.rowHeight,
forceSquare: Boolean(
(p.flag & this.c.FLAG_IS_FOLDER) | (p.flag & this.c.FLAG_IS_TAG)
),
forceSquare: Boolean(p.flag & this.c.FLAG_IS_FOLDER),
};
}),
{
@ -1419,13 +1348,7 @@ export default defineComponent({
}
}
/** Static and dynamic top matter */
.top-matter {
padding-top: 4px;
@include phone {
padding-left: 40px;
}
}
/** Dynamic top matter */
.recycler-before {
width: 100%;
> .text {

View File

@ -1,10 +1,10 @@
<template>
<router-link
draggable="false"
class="tag fill-block"
:class="{ face, error }"
class="cluster fill-block"
:class="{ error }"
:to="target"
@click.native="openTag(data)"
@click.native="click"
>
<div class="bbl">
<NcCounterBubble> {{ data.count }} </NcCounterBubble>
@ -32,29 +32,30 @@
<script lang="ts">
import { defineComponent, PropType } from "vue";
import { IAlbum, ITag } from "../../types";
import { IAlbum, ICluster, IFace } from "../../types";
import { getPreviewUrl } from "../../services/FileUtils";
import { getCurrentUser } from "@nextcloud/auth";
import NcCounterBubble from "@nextcloud/vue/dist/Components/NcCounterBubble";
import { constants } from "../../services/Utils";
import { API } from "../../services/API";
import Vue from "vue";
export default defineComponent({
name: "Tag",
name: "Cluster",
components: {
NcCounterBubble,
},
props: {
data: {
type: Object as PropType<ITag>,
type: Object as PropType<ICluster>,
required: true,
},
noNavigate: {
link: {
type: Boolean,
default: false,
default: true,
},
},
@ -65,15 +66,7 @@ export default defineComponent({
return getPreviewUrl(mock, true, 512);
}
if (this.face) {
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);
return API.CLUSTER_PREVIEW(this.data.cluster_type, this.data.cluster_id);
},
title() {
@ -93,35 +86,28 @@ export default defineComponent({
},
tag() {
return !this.face && !this.place && !this.album ? this.data : null;
return this.data.cluster_type === "tags" && this.data;
},
face() {
return this.data.flag & constants.c.FLAG_IS_FACE_RECOGNIZE ||
this.data.flag & constants.c.FLAG_IS_FACE_RECOGNITION
? this.data
: null;
},
faceApp() {
return this.data.flag & constants.c.FLAG_IS_FACE_RECOGNITION
? "facerecognition"
: "recognize";
return (
(this.data.cluster_type === "recognize" ||
this.data.cluster_type === "facerecognition") &&
(this.data as IFace)
);
},
place() {
return this.data.flag & constants.c.FLAG_IS_PLACE ? this.data : null;
return this.data.cluster_type === "places" && this.data;
},
album() {
return this.data.flag & constants.c.FLAG_IS_ALBUM
? <IAlbum>this.data
: null;
return this.data.cluster_type === "albums" && (this.data as IAlbum);
},
/** Target URL to navigate to */
target() {
if (this.noNavigate) return {};
if (!this.link) return {};
if (this.album) {
const user = this.album.user;
@ -130,13 +116,13 @@ export default defineComponent({
}
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;
return { name: this.faceApp, params: { name, user } };
return { name: this.data.cluster_type, params: { name, user } };
}
if (this.place) {
const id = this.place.fileid.toString();
const id = this.place.cluster_id;
const placeName = this.place.name || id;
const name = `${id}-${placeName}`;
return { name: "places", params: { name } };
@ -147,30 +133,24 @@ export default defineComponent({
error() {
return (
Boolean(this.data.flag & this.c.FLAG_LOAD_FAIL) ||
Boolean(this.data.previewError) ||
Boolean(this.album && this.album.last_added_photo <= 0)
);
},
},
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() {
this.data.flag |= this.c.FLAG_LOAD_FAIL;
Vue.set(this.data, "previewError", true);
},
click() {
this.$emit("click", this.data);
},
},
});
</script>
<style lang="scss" scoped>
.tag,
.cluster,
.name,
.bubble,
img {
@ -178,7 +158,7 @@ img {
}
// Get rid of color of the bubble
.tag .bbl :deep .counter-bubble__counter {
.cluster .bbl :deep .counter-bubble__counter {
color: unset !important;
}
@ -201,7 +181,7 @@ img {
display: block;
}
.tag.error > & {
.cluster.error > & {
color: unset;
}

View File

@ -6,15 +6,14 @@
:value.sync="search"
:label="t('memories', 'Search')"
:placeholder="t('memories', 'Search')"
@input="searchChanged"
>
<Magnify :size="16" />
</NcTextField>
</div>
<div v-if="detail">
<div class="photo" v-for="photo of detail" :key="photo.fileid">
<Tag :data="photo" :noNavigate="true" @open="clickFace" />
<div v-if="list">
<div class="photo" v-for="photo of filteredList" :key="photo.cluster_id">
<Cluster :data="photo" :link="false" @click="clickFace" />
</div>
</div>
<div v-else>
@ -25,8 +24,8 @@
<script lang="ts">
import { defineComponent } from "vue";
import { IPhoto, ITag } from "../../types";
import Tag from "../frame/Tag.vue";
import { ICluster, IFace } from "../../types";
import Cluster from "../frame/Cluster.vue";
import NcTextField from "@nextcloud/vue/dist/Components/NcTextField";
@ -38,7 +37,7 @@ import Magnify from "vue-material-design-icons/Magnify.vue";
export default defineComponent({
name: "FaceList",
components: {
Tag,
Cluster,
NcTextField,
Magnify,
},
@ -46,9 +45,8 @@ export default defineComponent({
data: () => ({
user: "",
name: "",
fullDetail: null as ITag[] | null,
detail: null as ITag[] | null,
fuse: null as Fuse<ITag>,
list: null as ICluster[] | null,
fuse: null as Fuse<ICluster>,
search: "",
}),
@ -62,6 +60,13 @@ export default defineComponent({
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: {
close() {
this.$emit("close");
@ -70,43 +75,22 @@ export default defineComponent({
async refreshParams() {
this.user = <string>this.$route.params.user || "";
this.name = <string>this.$route.params.name || "";
this.detail = null;
this.fullDetail = null;
this.list = null;
this.search = "";
let data = [];
let flags = this.c.FLAG_IS_TAG;
if (this.$route.name === "recognize") {
data = await dav.getPeopleData("recognize");
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.list = (await dav.getFaceList(this.$route.name as any)).filter(
(c: IFace) => {
const clusterName = String(c.name || c.cluster_id);
return c.user_id === this.user && clusterName !== this.name;
}
);
this.detail = detail;
this.fullDetail = detail;
this.fuse = new Fuse(detail, { keys: ["name"] });
this.fuse = new Fuse(this.list, { keys: ["name"] });
},
async clickFace(face: ITag) {
async clickFace(face: IFace) {
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>

View File

@ -35,8 +35,8 @@ const NcProgressBar = () =>
import { showError } from "@nextcloud/dialogs";
import { getCurrentUser } from "@nextcloud/auth";
import { IFileInfo, ITag } from "../../types";
import Tag from "../frame/Tag.vue";
import { IFileInfo, IFace } from "../../types";
import Cluster from "../frame/Cluster.vue";
import FaceList from "./FaceList.vue";
import Modal from "./Modal.vue";
@ -50,7 +50,7 @@ export default defineComponent({
NcTextField,
NcProgressBar,
Modal,
Tag,
Cluster,
FaceList,
},
@ -79,11 +79,11 @@ export default defineComponent({
this.show = true;
},
async clickFace(face: ITag) {
async clickFace(face: IFace) {
const user = this.$route.params.user || "";
const name = this.$route.params.name || "";
const newName = face.name || face.fileid.toString();
const newName = String(face.name || face.cluster_id);
if (
!confirm(
this.t(

View File

@ -24,8 +24,8 @@ const NcTextField = () => import("@nextcloud/vue/dist/Components/NcTextField");
import { showError } from "@nextcloud/dialogs";
import { getCurrentUser } from "@nextcloud/auth";
import { IPhoto, ITag } from "../../types";
import Tag from "../frame/Tag.vue";
import { IPhoto, IFace } from "../../types";
import Cluster from "../frame/Cluster.vue";
import FaceList from "./FaceList.vue";
import Modal from "./Modal.vue";
@ -38,7 +38,7 @@ export default defineComponent({
NcButton,
NcTextField,
Modal,
Tag,
Cluster,
FaceList,
},
@ -86,11 +86,11 @@ export default defineComponent({
this.$emit("moved", list);
},
async clickFace(face: ITag) {
async clickFace(face: IFace) {
const user = this.$route.params.user || "";
const name = this.$route.params.name || "";
const newName = face.name || face.fileid.toString();
const newName = String(face.name || face.cluster_id);
if (
!confirm(

View File

@ -206,26 +206,3 @@ export default defineComponent({
},
});
</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>

View File

@ -1,12 +1,12 @@
<template>
<div v-if="name" class="tag-top-matter">
<NcActions>
<div class="top-matter">
<NcActions v-if="name">
<NcActionButton :aria-label="t('memories', 'Back')" @click="back()">
{{ t("memories", "Back") }}
<template #icon> <BackIcon :size="20" /> </template>
</NcActionButton>
</NcActions>
<span class="name">{{ name }}</span>
<span class="name">{{ name || viewname }}</span>
</div>
</template>
@ -15,6 +15,7 @@ import { defineComponent } from "vue";
import NcActions from "@nextcloud/vue/dist/Components/NcActions";
import NcActionButton from "@nextcloud/vue/dist/Components/NcActionButton";
import * as strings from "../../services/strings";
import BackIcon from "vue-material-design-icons/ArrowLeft.vue";
@ -26,48 +27,27 @@ export default defineComponent({
BackIcon,
},
data: () => ({
name: "",
}),
watch: {
$route: function (from: any, to: any) {
this.createMatter();
computed: {
viewname(): string {
return strings.viewName(this.$route.name);
},
},
mounted() {
this.createMatter();
name(): string | null {
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: {
createMatter() {
this.name = <string>this.$route.params.name || "";
if (this.$route.name === "places") {
this.name = this.name.split("-").slice(1).join("-");
}
},
back() {
this.$router.push({ name: this.$route.name });
},
},
});
</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>

View File

@ -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>

View File

@ -119,26 +119,3 @@ export default defineComponent({
},
});
</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>

View File

@ -131,23 +131,8 @@ export default defineComponent({
<style lang="scss" scoped>
.top-matter {
display: flex;
vertical-align: middle;
.breadcrumb {
min-width: 0;
}
.right-actions {
margin-right: 40px;
z-index: 50;
@media (max-width: 768px) {
margin-right: 10px;
}
:deep span {
cursor: pointer;
}
}
}
</style>

View File

@ -1,7 +1,7 @@
<template>
<div class="top-matter" v-if="type">
<div class="top-matter-container" v-if="type">
<FolderTopMatter v-if="type === 1" />
<TagTopMatter v-else-if="type === 2" />
<ClusterTopMatter v-else-if="type === 2" />
<FaceTopMatter v-else-if="type === 3" />
<AlbumTopMatter v-else-if="type === 4" />
</div>
@ -11,7 +11,7 @@
import { defineComponent } from "vue";
import FolderTopMatter from "./FolderTopMatter.vue";
import TagTopMatter from "./TagTopMatter.vue";
import ClusterTopMatter from "./ClusterTopMatter.vue";
import FaceTopMatter from "./FaceTopMatter.vue";
import AlbumTopMatter from "./AlbumTopMatter.vue";
@ -21,7 +21,7 @@ export default defineComponent({
name: "TopMatter",
components: {
FolderTopMatter,
TagTopMatter,
ClusterTopMatter,
FaceTopMatter,
AlbumTopMatter,
},
@ -47,21 +47,16 @@ export default defineComponent({
switch (this.$route.name) {
case "folders":
return TopMatterType.FOLDER;
case "albums":
return TopMatterType.ALBUM;
case "tags":
return this.$route.params.name
? TopMatterType.TAG
: TopMatterType.NONE;
case "places":
return TopMatterType.CLUSTER;
case "recognize":
case "facerecognition":
return this.$route.params.name
? TopMatterType.FACE
: TopMatterType.NONE;
case "albums":
return TopMatterType.ALBUM;
case "places":
return this.$route.params.name
? TopMatterType.TAG
: TopMatterType.NONE;
: TopMatterType.CLUSTER;
default:
return TopMatterType.NONE;
}
@ -70,3 +65,46 @@ export default defineComponent({
},
});
</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>

View File

@ -94,13 +94,21 @@ body.has-viewer header {
}
// Hide scrollbar
.recycler::-webkit-scrollbar {
display: none;
width: 0 !important;
}
.recycler {
@mixin hide-scrollbar {
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
width: 0 !important;
}
}
.hide-scrollbar {
@include hide-scrollbar;
}
.hide-scrollbar-mobile {
@media (max-width: 768px) {
@include hide-scrollbar;
}
}
// Make metadata tab scrollbar thin

View File

@ -4,6 +4,7 @@ import Router from "vue-router";
import Vue from "vue";
import Timeline from "./components/Timeline.vue";
import SplitTimeline from "./components/SplitTimeline.vue";
import ClusterView from "./components/ClusterView.vue";
Vue.use(Router);
@ -52,7 +53,7 @@ export default new Router({
{
path: "/albums/:user?/:name?",
component: Timeline,
component: ClusterView,
name: "albums",
props: (route) => ({
rootTitle: t("memories", "Albums"),
@ -79,7 +80,7 @@ export default new Router({
{
path: "/recognize/:user?/:name?",
component: Timeline,
component: ClusterView,
name: "recognize",
props: (route) => ({
rootTitle: t("memories", "People"),
@ -88,7 +89,7 @@ export default new Router({
{
path: "/facerecognition/:user?/:name?",
component: Timeline,
component: ClusterView,
name: "facerecognition",
props: (route) => ({
rootTitle: t("memories", "People"),
@ -97,7 +98,7 @@ export default new Router({
{
path: "/places/:name*",
component: Timeline,
component: ClusterView,
name: "places",
props: (route) => ({
rootTitle: t("memories", "Places"),
@ -106,7 +107,7 @@ export default new Router({
{
path: "/tags/:name*",
component: Timeline,
component: ClusterView,
name: "tags",
props: (route) => ({
rootTitle: t("memories", "Tags"),

View File

@ -1,4 +1,5 @@
import { generateUrl } from "@nextcloud/router";
import { ClusterTypes } from "../types";
const BASE = "/apps/memories/api";
@ -65,7 +66,7 @@ export class API {
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;
}
@ -74,25 +75,20 @@ export class API {
}
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() {
return gen(`${BASE}/clusters/places`);
}
static PLACE_PREVIEW(place: number | string) {
return gen(`${BASE}/clusters/places/preview/{place}`, { place });
}
static TAG_LIST() {
return gen(`${BASE}/clusters/tags`);
}
static TAG_PREVIEW(tag: string) {
return gen(`${BASE}/clusters/tags/preview/{tag}`, { tag });
}
static TAG_SET(fileid: string | number) {
return gen(`${BASE}/tags/set/{fileid}`, { fileid });
}
@ -101,11 +97,8 @@ export class API {
return gen(`${BASE}/clusters/${app}`);
}
static FACE_PREVIEW(
app: "recognize" | "facerecognition",
face: string | number
) {
return gen(`${BASE}/clusters/${app}/preview/{face}`, { face });
static CLUSTER_PREVIEW(backend: ClusterTypes, name: string | number) {
return gen(`${BASE}/clusters/${backend}/preview/{name}`, { name });
}
static ARCHIVE(fileid: number) {

View File

@ -212,27 +212,6 @@ export function convertFlags(photo: IPhoto) {
photo.flag |= constants.c.FLAG_IS_FOLDER;
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_FAVORITE: 1 << 3,
FLAG_IS_FOLDER: 1 << 4,
FLAG_IS_ALBUM: 1 << 5,
FLAG_IS_FACE_RECOGNIZE: 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,
FLAG_SELECTED: 1 << 5,
FLAG_LEAVING: 1 << 6,
},
TagDayID: TagDayID,

View File

@ -1,9 +1,8 @@
import * as base from "./base";
import { getCurrentUser } from "@nextcloud/auth";
import { showError } from "@nextcloud/dialogs";
import { translate as t, translatePlural as n } from "@nextcloud/l10n";
import { IAlbum, IDay, IFileInfo, IPhoto, ITag } from "../../types";
import { constants } from "../Utils";
import { translate as t } from "@nextcloud/l10n";
import { IAlbum, IFileInfo, IPhoto } from "../../types";
import { API } from "../API";
import axios from "@nextcloud/axios";
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 sortOrder Sort order; 1 = by date, 2 = by name
*/
export async function getAlbumsData(
type: 1 | 2 | 3,
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;
}
export async function getAlbums(type: 1 | 2 | 3, sortOrder: 1 | 2) {
const data = (await axios.get<IAlbum[]>(API.ALBUM_LIST(type))).data;
// Response is already sorted by date, sort otherwise
if (sortOrder === 2) {
@ -45,23 +35,7 @@ export async function getAlbumsData(
);
}
// Convert to days response
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)
),
},
];
return data;
}
/**

View File

@ -2,47 +2,13 @@ import axios from "@nextcloud/axios";
import { showError } from "@nextcloud/dialogs";
import { translate as t } from "@nextcloud/l10n";
import { generateUrl } from "@nextcloud/router";
import { IDay, IPhoto } from "../../types";
import { IFace, IPhoto } from "../../types";
import { API } from "../API";
import { constants } from "../Utils";
import client from "../DavClient";
import * as base from "./base";
/**
* Get list of tags and convert to Days response
*/
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 getFaceList(app: "recognize" | "facerecognition") {
return (await axios.get<IFace[]>(API.FACE_LIST(app))).data;
}
export async function updatePeopleFaceRecognition(

View File

@ -1,41 +1,7 @@
import { IDay, IPhoto, ITag } from "../../types";
import { constants } from "../Utils";
import { ICluster } from "../../types";
import { API } from "../API";
import axios from "@nextcloud/axios";
/**
* Get list of tags and convert to Days response
*/
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)
),
},
];
export async function getPlaces() {
return (await axios.get<ICluster[]>(API.PLACE_LIST())).data;
}

View File

@ -1,39 +1,10 @@
import { IDay, IPhoto, ITag } from "../../types";
import { constants, hashCode } from "../Utils";
import { ICluster } from "../../types";
import { API } from "../API";
import axios from "@nextcloud/axios";
/**
* Get list of tags and convert to Days response
* Get list of tags.
*/
export async function getTagsData(): Promise<IDay[]> {
// Query for photos
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)
),
},
];
export async function getTags() {
return (await axios.get<ICluster[]>(API.TAG_LIST())).data;
}

View File

@ -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 "";
}
}

View File

@ -117,18 +117,28 @@ export interface IFolder extends IPhoto {
name: string;
}
export interface ITag extends IPhoto {
/** Name of tag */
name: string;
/** Number of images in this tag */
export type ClusterTypes =
| "tags"
| "albums"
| "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;
/** User for face if face */
user_id?: string;
/** Cache of previews */
previews?: IPhoto[];
/** Name of cluster */
name: string;
/** Preview loading failed */
previewError?: boolean;
}
export interface IAlbum extends ITag {
export interface IAlbum extends ICluster {
/** ID of album */
album_id: number;
/** Owner of album */
@ -141,6 +151,11 @@ export interface IAlbum extends ITag {
last_added_photo: number;
}
export interface IFace extends ICluster {
/** User for face */
user_id: string;
}
export interface IFaceRect {
w: number;
h: number;
@ -211,7 +226,7 @@ export type TopMatter = {
export enum TopMatterType {
NONE = 0,
FOLDER = 1,
TAG = 2,
CLUSTER = 2,
FACE = 3,
ALBUM = 4,
}