Add album form
parent
67206643b7
commit
c9a2c8a021
|
@ -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');
|
||||
|
|
|
@ -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>
|
|
@ -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) {
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -655,3 +655,65 @@ 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
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue