Add album form

old-stable24
Varun Patil 2022-10-26 23:37:56 -07:00
parent 67206643b7
commit c9a2c8a021
6 changed files with 451 additions and 10 deletions

View File

@ -37,15 +37,15 @@ trait TimelineQueryAlbums
);
// WHERE these are items with this album
$query->innerJoin('pa', 'photos_albums_files', 'paf', $query->expr()->andX(
$query->leftJoin('pa', 'photos_albums_files', 'paf', $query->expr()->andX(
$query->expr()->eq('paf.album_id', 'pa.album_id'),
));
// WHERE these items are memories indexed photos
$query->innerJoin('paf', 'memories', 'm', $query->expr()->eq('m.fileid', 'paf.file_id'));
$query->leftJoin('paf', 'memories', 'm', $query->expr()->eq('m.fileid', 'paf.file_id'));
// WHERE these photos are in the filecache
$query->innerJoin('m', 'filecache', 'f', $query->expr()->eq('m.fileid', 'f.fileid'));
$query->leftJoin('m', 'filecache', 'f', $query->expr()->eq('m.fileid', 'f.fileid'));
// GROUP and ORDER by
$query->groupBy('pa.album_id');

View File

@ -0,0 +1,263 @@
<!--
- @copyright Copyright (c) 2022 Louis Chemineau <louis@chmn.me>
-
- @author Louis Chemineau <louis@chmn.me>
-
- @license AGPL-3.0-or-later
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-->
<template>
<form v-if="!showCollaboratorView" class="album-form" @submit.prevent="submit">
<div class="form-inputs">
<NcTextField ref="nameInput"
:value.sync="albumName"
type="text"
name="name"
:required="true"
autofocus="true"
:placeholder="t('photos', 'Name of the album')" />
<label>
<NcTextField :value.sync="albumLocation"
name="location"
type="text"
:placeholder="t('photos', 'Location of the album')" />
</label>
</div>
<div class="form-buttons">
<span class="left-buttons">
<NcButton v-if="displayBackButton"
:aria-label="t('photos', 'Go back to the previous view.')"
type="tertiary"
@click="back">
{{ t('photos', 'Back') }}
</NcButton>
</span>
<span class="right-buttons">
<NcButton v-if="sharingEnabled && !editMode"
:aria-label="t('photos', 'Go to the add collaborators view.')"
type="secondary"
:disabled="albumName.trim() === '' || loading"
@click="showCollaboratorView = true">
<template #icon>
<AccountMultiplePlus />
</template>
{{ t('photos', 'Add collaborators') }}
</NcButton>
<NcButton :aria-label="editMode ? t('photos', 'Save.') : t('photos', 'Create the album.')"
type="primary"
:disabled="albumName === '' || loading"
@click="submit()">
<template #icon>
<NcLoadingIcon v-if="loading" />
<Send v-else />
</template>
{{ editMode ? t('photos', 'Save') : t('photos', 'Create album') }}
</NcButton>
</span>
</div>
</form>
<!-- <CollaboratorsSelectionForm v-else
:album-name="albumName"
:allow-public-link="false">
<template slot-scope="{collaborators}">
<span class="left-buttons">
<NcButton :aria-label="t('photos', 'Back to the new album form.')"
type="tertiary"
@click="showCollaboratorView = false">
{{ t('photos', 'Back') }}
</NcButton>
</span>
<span class="right-buttons">
<NcButton :aria-label="editMode ? t('photos', 'Save.') : t('photos', 'Create the album.')"
type="primary"
:disabled="albumName.trim() === '' || loading"
@click="submit(collaborators)">
<template #icon>
<NcLoadingIcon v-if="loading" />
<Send v-else />
</template>
{{ editMode ? t('photos', 'Save') : t('photos', 'Create album') }}
</NcButton>
</span>
</template>
</CollaboratorsSelectionForm> -->
</template>
<script lang="ts">
import { Component, Emit, Mixins, Prop } from 'vue-property-decorator';
import GlobalMixin from '../../mixins/GlobalMixin';
import { getCurrentUser } from '@nextcloud/auth'
import { NcButton, NcLoadingIcon, NcTextField } from '@nextcloud/vue'
import { IAlbum } from '../../types';
import moment from 'moment';
import * as dav from '../../services/DavRequests';
// import CollaboratorsSelectionForm from './CollaboratorsSelectionForm.vue'
import Send from 'vue-material-design-icons/Send.vue'
import AccountMultiplePlus from 'vue-material-design-icons/AccountMultiplePlus.vue'
@Component({
components: {
NcButton,
NcLoadingIcon,
NcTextField,
// CollaboratorsSelectionForm,
Send,
AccountMultiplePlus,
},
})
export default class AlbumForm extends Mixins(GlobalMixin) {
@Prop() private album: IAlbum;
@Prop() private displayBackButton: boolean;
private showCollaboratorView = false;
private albumName = '';
private albumLocation = '';
private loading = false;
/**
* @return Whether sharing is enabled.
*/
get editMode(): boolean {
return Boolean(this.album);
}
/**
* @return Whether sharing is enabled.
*/
get sharingEnabled(): boolean {
return window.OC.Share !== undefined
}
mounted() {
if (this.editMode) {
this.albumName = this.album.name
this.albumLocation = this.album.location
}
this.$nextTick(() => {
(<any>this.$refs.nameInput).$el.getElementsByTagName('input')[0].focus()
})
}
submit(collaborators = []) {
if (this.albumName === '' || this.loading) {
return
}
if (this.editMode) {
this.handleUpdateAlbum()
} else {
this.handleCreateAlbum(collaborators)
}
}
async handleCreateAlbum(collaborators = []) {
try {
this.loading = true
let album = {
basename: this.albumName,
filename: `/photos/${getCurrentUser().uid}/albums/${this.albumName}`,
nbItems: 0,
location: this.albumLocation,
lastPhoto: -1,
date: moment().format('MMMM YYYY'),
collaborators,
}
await dav.createAlbum(album.basename);
if (this.albumLocation !== '' || collaborators.length !== 0) {
album = await dav.updateAlbum(album, {
albumName: this.albumName,
properties: {
location: this.albumLocation,
collaborators,
},
});
}
this.$emit('done', { album })
} finally {
this.loading = false
}
}
async handleUpdateAlbum() {
try {
this.loading = true
let album = { ...this.album }
if (this.album.name !== this.albumName) {
album = await this.renameAlbum({ currentAlbumName: this.album.name, newAlbumName: this.albumName })
}
if (this.album.location !== this.albumLocation) {
album.location = await this.updateAlbum({ albumName: this.albumName, properties: { location: this.albumLocation } })
}
this.$emit('done', { album })
} finally {
this.loading = false
}
}
@Emit('back')
back() {}
}
</script>
<style lang="scss" scoped>
.album-form {
display: flex;
flex-direction: column;
height: 350px;
padding: 16px;
.form-title {
font-weight: bold;
}
.form-subtitle {
color: var(--color-text-lighter);
}
.form-inputs {
flex-grow: 1;
justify-items: flex-end;
input {
width: 100%;
}
label {
display: flex;
margin-top: 16px;
:deep svg {
margin-right: 12px;
}
}
}
.form-buttons {
display: flex;
justify-content: space-between;
.left-buttons, .right-buttons {
display: flex;
}
.right-buttons {
justify-content: flex-end;
}
button {
margin-right: 16px;
}
}
}
.left-buttons {
flex-grow: 1;
}
</style>

View File

@ -56,17 +56,18 @@
</NcButton>
</div>
<!-- <AlbumForm v-else
<AlbumForm v-else
:display-back-button="true"
:title="t('photos', 'New album')"
@back="showAlbumCreationForm = false"
@done="albumCreatedHandler" /> -->
@done="albumCreatedHandler" />
</template>
<script lang="ts">
import { Component, Emit, Mixins } from 'vue-property-decorator';
import GlobalMixin from '../../mixins/GlobalMixin';
import AlbumForm from './AlbumForm.vue'
import Plus from 'vue-material-design-icons/Plus.vue'
import ImageMultiple from 'vue-material-design-icons/ImageMultiple.vue'
@ -75,16 +76,14 @@ import { generateUrl } from '@nextcloud/router'
import { IAlbum } from '../../types';
import axios from '@nextcloud/axios'
// import AlbumForm from './AlbumForm.vue'
@Component({
components: {
AlbumForm,
Plus,
ImageMultiple,
NcButton,
NcListItem,
NcLoadingIcon,
// AlbumForm,
},
filters: {
toCoverUrl(fileId: string) {

View File

@ -18,6 +18,8 @@
<script lang="ts">
import { Component, Emit, Mixins, Prop } from 'vue-property-decorator';
import GlobalMixin from '../../mixins/GlobalMixin';
import { NcButton, NcTextField } from '@nextcloud/vue';
import { showError } from '@nextcloud/dialogs';
import { getCurrentUser } from '@nextcloud/auth';
@ -26,7 +28,6 @@ import Tag from '../frame/Tag.vue';
import FaceList from './FaceList.vue';
import Modal from './Modal.vue';
import GlobalMixin from '../../mixins/GlobalMixin';
import client from '../../services/DavClient';
import * as dav from '../../services/DavRequests';

View File

@ -0,0 +1,116 @@
<template>
<div v-if="name" class="face-top-matter">
<NcActions>
<NcActionButton :aria-label="t('memories', 'Back')" @click="back()">
{{ t('memories', 'Back') }}
<template #icon> <BackIcon :size="20" /> </template>
</NcActionButton>
</NcActions>
<div class="name">{{ name }}</div>
<div class="right-actions">
<NcActions :inline="1">
<NcActionButton :aria-label="t('memories', 'Rename person')" @click="$refs.editModal.open()" close-after-click>
{{ t('memories', 'Rename person') }}
<template #icon> <EditIcon :size="20" /> </template>
</NcActionButton>
<NcActionButton :aria-label="t('memories', 'Merge with different person')" @click="$refs.mergeModal.open()" close-after-click>
{{ t('memories', 'Merge with different person') }}
<template #icon> <MergeIcon :size="20" /> </template>
</NcActionButton>
<NcActionCheckbox :aria-label="t('memories', 'Mark person in preview')" :checked.sync="config_showFaceRect" @change="changeShowFaceRect">
{{ t('memories', 'Mark person in preview') }}
</NcActionCheckbox>
<NcActionButton :aria-label="t('memories', 'Remove person')" @click="$refs.deleteModal.open()" close-after-click>
{{ t('memories', 'Remove person') }}
<template #icon> <DeleteIcon :size="20" /> </template>
</NcActionButton>
</NcActions>
</div>
<FaceEditModal ref="editModal" />
<FaceDeleteModal ref="deleteModal" />
<FaceMergeModal ref="mergeModal" />
</div>
</template>
<script lang="ts">
import { Component, Mixins, Watch } from 'vue-property-decorator';
import GlobalMixin from '../../mixins/GlobalMixin';
import UserConfig from "../../mixins/UserConfig";
import { NcActions, NcActionButton, NcActionCheckbox } from '@nextcloud/vue';
import FaceEditModal from '../modal/FaceEditModal.vue';
import FaceDeleteModal from '../modal/FaceDeleteModal.vue';
import FaceMergeModal from '../modal/FaceMergeModal.vue';
import BackIcon from 'vue-material-design-icons/ArrowLeft.vue';
import EditIcon from 'vue-material-design-icons/Pencil.vue';
import DeleteIcon from 'vue-material-design-icons/Close.vue';
import MergeIcon from 'vue-material-design-icons/Merge.vue';
@Component({
components: {
NcActions,
NcActionButton,
NcActionCheckbox,
FaceEditModal,
FaceDeleteModal,
FaceMergeModal,
BackIcon,
EditIcon,
DeleteIcon,
MergeIcon,
},
})
export default class FaceTopMatter extends Mixins(GlobalMixin, UserConfig) {
private name: string = '';
@Watch('$route')
async routeChange(from: any, to: any) {
this.createMatter();
}
mounted() {
this.createMatter();
}
createMatter() {
this.name = this.$route.params.name || '';
}
back() {
this.$router.push({ name: 'people' });
}
changeShowFaceRect() {
localStorage.setItem('memories_showFaceRect', this.config_showFaceRect ? '1' : '0');
setTimeout(() => {
this.$router.go(0); // refresh page
}, 500);
}
}
</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

@ -654,4 +654,66 @@ export async function* removeFromAlbum(id: number, fileIds: number[]) {
});
yield* runInParallel(calls, 10);
}
}
/**
* Create an album.
*/
export async function createAlbum(albumName: string) {
try {
await client.createDirectory(`/photos/${getCurrentUser()?.uid}/albums/${albumName}`)
} catch (error) {
console.error(error);
showError(t('photos', 'Failed to create {albumName}.', { albumName }))
}
}
/**
* Update an album's properties.
*
* @param {object} album Album to update
* @param {object} data destructuring object
* @param {string} data.albumName - The name of the album.
* @param {object} data.properties - The properties to update.
*/
export async function updateAlbum(album: any, { albumName, properties }: any) {
const stringifiedProperties = Object
.entries(properties)
.map(([name, value]) => {
switch (typeof value) {
case 'string':
return `<nc:${name}>${value}</nc:${name}>`
case 'object':
return `<nc:${name}>${JSON.stringify(value)}</nc:${name}>`
default:
return ''
}
})
.join()
try {
await client.customRequest(
album.filename,
{
method: 'PROPPATCH',
data: `<?xml version="1.0"?>
<d:propertyupdate xmlns:d="DAV:"
xmlns:oc="http://owncloud.org/ns"
xmlns:nc="http://nextcloud.org/ns"
xmlns:ocs="http://open-collaboration-services.org/ns">
<d:set>
<d:prop>
${stringifiedProperties}
</d:prop>
</d:set>
</d:propertyupdate>`,
}
);
return album;
} catch (error) {
console.error(error);
showError(t('photos', 'Failed to update properties of {albumName} with {properties}.', { albumName, properties: JSON.stringify(properties) }))
return album
}
};