mirror of https://github.com/nextcloud/photos
441 lines
12 KiB
Vue
441 lines
12 KiB
Vue
<!--
|
|
- @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>
|
|
<div>
|
|
<CollectionContent v-if="true"
|
|
ref="collectionContent"
|
|
:collection="album"
|
|
:collection-file-ids="albumFileIds"
|
|
:semaphore="semaphore"
|
|
:loading="loadingAlbums || loadingFiles"
|
|
:error="errorFetchingAlbums || errorFetchingFiles">
|
|
<!-- Header -->
|
|
<HeaderNavigation key="navigation"
|
|
slot="header"
|
|
slot-scope="{selectedFileIds}"
|
|
:loading="loadingFiles"
|
|
:params="{ albumName }"
|
|
:path="'/' + albumName"
|
|
:title="albumName"
|
|
@refresh="fetchAlbumContent">
|
|
<div v-if="album.location !== ''" slot="subtitle" class="album__location">
|
|
<MapMarker />{{ album.location }}
|
|
</div>
|
|
<template v-if="album !== undefined"
|
|
slot="right">
|
|
<UploadPicker v-if="album.nbItems !== 0"
|
|
:accept="allowedMimes"
|
|
:context="uploadContext"
|
|
:destination="album.basename"
|
|
:root="uploadContext.root"
|
|
:multiple="true"
|
|
@uploaded="onUpload" />
|
|
|
|
<NcButton v-if="sharingEnabled"
|
|
type="tertiary"
|
|
:aria-label="t('photos', 'Manage collaborators for this album')"
|
|
@click="showManageCollaboratorView = true">
|
|
<ShareVariant slot="icon" />
|
|
</NcButton>
|
|
|
|
<NcActions :aria-label="t('photos', 'Open actions menu')">
|
|
<NcActionButton :close-after-click="true"
|
|
:aria-label="t('photos', 'Edit album details')"
|
|
@click="showEditAlbumForm = true">
|
|
{{ t('photos', 'Edit album details') }}
|
|
<Pencil slot="icon" />
|
|
</NcActionButton>
|
|
|
|
<ActionDownload v-if="albumFileIds.length > 0"
|
|
:selected-file-ids="albumFileIds"
|
|
:title="t('photos', 'Download all files in album')">
|
|
<DownloadMultiple slot="icon" />
|
|
</ActionDownload>
|
|
|
|
<NcActionButton :close-after-click="true"
|
|
@click="handleDeleteAlbum">
|
|
{{ t('photos', 'Delete album') }}
|
|
<Delete slot="icon" />
|
|
</NcActionButton>
|
|
|
|
<template v-if="selectedFileIds.length > 0">
|
|
<NcActionSeparator />
|
|
|
|
<ActionDownload :selected-file-ids="selectedFileIds" :title="t('photos', 'Download selected files')">
|
|
<Download slot="icon" />
|
|
</ActionDownload>
|
|
|
|
<ActionFavorite :selected-file-ids="selectedFileIds" />
|
|
|
|
<NcActionButton :close-after-click="true"
|
|
@click="handleRemoveFilesFromAlbum(selectedFileIds)">
|
|
{{ t('photos', 'Remove selection from album') }}
|
|
<Close slot="icon" />
|
|
</NcActionButton>
|
|
</template>
|
|
</NcActions>
|
|
</template>
|
|
</HeaderNavigation>
|
|
|
|
<!-- No content -->
|
|
<NcEmptyContent v-if="album !== undefined && album.nbItems === 0 && !(loadingFiles || loadingAlbums)"
|
|
slot="empty-content"
|
|
:title="t('photos', 'This album does not have any photos or videos yet!')"
|
|
class="album__empty">
|
|
<ImagePlus slot="icon" />
|
|
|
|
<NcButton slot="action"
|
|
class="album__empty__button"
|
|
type="primary"
|
|
:aria-label="t('photos', 'Add photos to this album')"
|
|
@click="showAddPhotosModal = true">
|
|
<Plus slot="icon" />
|
|
{{ t('photos', "Add") }}
|
|
</NcButton>
|
|
</NcEmptyContent>
|
|
</CollectionContent>
|
|
|
|
<NcModal v-if="showAddPhotosModal"
|
|
size="large"
|
|
:title="t('photos', 'Add photos to the album')"
|
|
@close="showAddPhotosModal = false">
|
|
<FilesPicker :destination="album.basename"
|
|
:blacklist-ids="albumFileIds"
|
|
@files-picked="handleFilesPicked" />
|
|
</NcModal>
|
|
|
|
<NcModal v-if="showManageCollaboratorView"
|
|
:title="t('photos', 'Manage collaborators')"
|
|
@close="showManageCollaboratorView = false">
|
|
<CollaboratorsSelectionForm :album-name="album.basename"
|
|
:collaborators="album.collaborators"
|
|
:public-link="album.publicLink">
|
|
<template slot-scope="{collaborators}">
|
|
<NcButton :aria-label="t('photos', 'Save collaborators for this album.')"
|
|
type="primary"
|
|
:disabled="loadingAddCollaborators"
|
|
@click="handleSetCollaborators(collaborators)">
|
|
<template #icon>
|
|
<NcLoadingIcon v-if="loadingAddCollaborators" />
|
|
</template>
|
|
{{ t('photos', 'Save') }}
|
|
</NcButton>
|
|
</template>
|
|
</CollaboratorsSelectionForm>
|
|
</NcModal>
|
|
|
|
<NcModal v-if="showEditAlbumForm"
|
|
:title="t('photos', 'Edit album details')"
|
|
@close="showEditAlbumForm = false">
|
|
<AlbumForm :album="album" @done="redirectToNewName" />
|
|
</NcModal>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
// eslint-disable-next-line node/no-extraneous-import
|
|
import { addNewFileMenuEntry } from '@nextcloud/files'
|
|
import { getCurrentUser } from '@nextcloud/auth'
|
|
import { mapActions, mapGetters } from 'vuex'
|
|
import { NcActions, NcActionButton, NcButton, NcModal, NcEmptyContent, NcActionSeparator, NcLoadingIcon, isMobile } from '@nextcloud/vue'
|
|
import { Upload, UploadPicker } from '@nextcloud/upload'
|
|
import debounce from 'debounce'
|
|
|
|
import Close from 'vue-material-design-icons/Close'
|
|
import Delete from 'vue-material-design-icons/Delete'
|
|
import Download from 'vue-material-design-icons/Download'
|
|
import DownloadMultiple from 'vue-material-design-icons/DownloadMultiple'
|
|
import ImagePlus from 'vue-material-design-icons/ImagePlus'
|
|
import MapMarker from 'vue-material-design-icons/MapMarker'
|
|
import Pencil from 'vue-material-design-icons/Pencil'
|
|
import Plus from 'vue-material-design-icons/Plus'
|
|
import PlusSvg from '@mdi/svg/svg/plus.svg'
|
|
import ShareVariant from 'vue-material-design-icons/ShareVariant'
|
|
|
|
import AbortControllerMixin from '../mixins/AbortControllerMixin.js'
|
|
import FetchAlbumsMixin from '../mixins/FetchAlbumsMixin.js'
|
|
import FetchFilesMixin from '../mixins/FetchFilesMixin.js'
|
|
import UserConfig from '../mixins/UserConfig.js'
|
|
|
|
import ActionDownload from '../components/Actions/ActionDownload.vue'
|
|
import ActionFavorite from '../components/Actions/ActionFavorite.vue'
|
|
import AlbumForm from '../components/Albums/AlbumForm.vue'
|
|
import CollaboratorsSelectionForm from '../components/Albums/CollaboratorsSelectionForm.vue'
|
|
import CollectionContent from '../components/Collection/CollectionContent.vue'
|
|
import FilesPicker from '../components/FilesPicker.vue'
|
|
import HeaderNavigation from '../components/HeaderNavigation.vue'
|
|
|
|
import { genFileInfo } from '../utils/fileUtils.js'
|
|
import allowedMimes from '../services/AllowedMimes.js'
|
|
import client from '../services/DavClient.js'
|
|
import DavRequest from '../services/DavRequest.js'
|
|
import logger from '../services/logger.js'
|
|
|
|
export default {
|
|
name: 'AlbumContent',
|
|
components: {
|
|
ActionDownload,
|
|
ActionFavorite,
|
|
AlbumForm,
|
|
Close,
|
|
CollaboratorsSelectionForm,
|
|
CollectionContent,
|
|
Delete,
|
|
Download,
|
|
DownloadMultiple,
|
|
FilesPicker,
|
|
HeaderNavigation,
|
|
ImagePlus,
|
|
MapMarker,
|
|
NcActionButton,
|
|
NcActions,
|
|
NcActionSeparator,
|
|
NcButton,
|
|
NcEmptyContent,
|
|
NcLoadingIcon,
|
|
NcModal,
|
|
Pencil,
|
|
Plus,
|
|
ShareVariant,
|
|
UploadPicker,
|
|
},
|
|
|
|
mixins: [
|
|
AbortControllerMixin,
|
|
FetchAlbumsMixin,
|
|
FetchFilesMixin,
|
|
isMobile,
|
|
UserConfig,
|
|
],
|
|
|
|
props: {
|
|
albumName: {
|
|
type: String,
|
|
default: '/',
|
|
},
|
|
},
|
|
|
|
data() {
|
|
return {
|
|
allowedMimes,
|
|
|
|
showAddPhotosModal: false,
|
|
showManageCollaboratorView: false,
|
|
showEditAlbumForm: false,
|
|
|
|
loadingAddCollaborators: false,
|
|
}
|
|
},
|
|
|
|
computed: {
|
|
...mapGetters([
|
|
'albumsFiles',
|
|
]),
|
|
|
|
/**
|
|
* @return {object} The album information for the current albumName.
|
|
*/
|
|
album() {
|
|
return this.albums[this.albumName] || {}
|
|
},
|
|
|
|
/**
|
|
* @return {string[]} The list of files for the current albumName.
|
|
*/
|
|
albumFileIds() {
|
|
return this.albumsFiles[this.albumName] || []
|
|
},
|
|
|
|
/**
|
|
* @return {boolean} Whether sharing is enabled.
|
|
*/
|
|
sharingEnabled() {
|
|
return OC.Share !== undefined
|
|
},
|
|
|
|
/**
|
|
* The upload picker context
|
|
* We're uploading to the album folder, and the backend handle
|
|
* the writing to the default location as well as the album update.
|
|
* The context is also used for the NewFileMenu.
|
|
*/
|
|
uploadContext() {
|
|
return {
|
|
...this.album,
|
|
route: this.$route.name,
|
|
root: `dav/photos/${getCurrentUser()?.uid}/albums`,
|
|
}
|
|
},
|
|
},
|
|
|
|
watch: {
|
|
album() {
|
|
this.fetchAlbumContent()
|
|
},
|
|
},
|
|
|
|
mounted() {
|
|
addNewFileMenuEntry({
|
|
id: 'album-add',
|
|
displayName: t('photos', 'Add photos to this album'),
|
|
templateName: '',
|
|
if: (context) => context.route === this.$route.name,
|
|
/** Existing icon css class */
|
|
iconSvgInline: PlusSvg,
|
|
/** Function to be run after creation */
|
|
handler: () => { this.showAddPhotosModal = true },
|
|
})
|
|
},
|
|
|
|
methods: {
|
|
...mapActions([
|
|
'appendFiles',
|
|
'deleteAlbum',
|
|
'addFilesToAlbum',
|
|
'removeFilesFromAlbum',
|
|
'updateAlbum',
|
|
]),
|
|
|
|
async fetchAlbumContent() {
|
|
if (this.loadingFiles || this.showEditAlbumForm) {
|
|
return []
|
|
}
|
|
|
|
const semaphoreSymbol = await this.semaphore.acquire(() => 0, 'fetchFiles')
|
|
const fetchSemaphoreSymbol = await this.fetchSemaphore.acquire()
|
|
|
|
try {
|
|
this.errorFetchingFiles = null
|
|
this.loadingFiles = true
|
|
this.semaphoreSymbol = semaphoreSymbol
|
|
|
|
const response = await client.getDirectoryContents(
|
|
`/photos/${getCurrentUser()?.uid}/albums/${this.albumName}`,
|
|
{
|
|
data: DavRequest,
|
|
details: true,
|
|
signal: this.abortController.signal,
|
|
}
|
|
)
|
|
|
|
// Gen files info and filtering invalid files
|
|
const fetchedFiles = response.data
|
|
.map(file => genFileInfo(file))
|
|
.filter(file => file.fileid)
|
|
|
|
const fileIds = fetchedFiles
|
|
.map(file => file.fileid.toString())
|
|
|
|
this.appendFiles(fetchedFiles)
|
|
|
|
if (fetchedFiles.length > 0) {
|
|
await this.$store.commit('addFilesToAlbum', { albumName: this.albumName, fileIdsToAdd: fileIds })
|
|
}
|
|
|
|
logger.debug(`[AlbumContent] Fetched ${fileIds.length} new files: `, fileIds)
|
|
} catch (error) {
|
|
if (error.response?.status === 404) {
|
|
this.errorFetchingFiles = 404
|
|
} else if (error.code === 'ERR_CANCELED') {
|
|
return
|
|
} else {
|
|
this.errorFetchingFiles = error
|
|
}
|
|
|
|
logger.error('[AlbumContent] Error fetching album files', error)
|
|
} finally {
|
|
this.loadingFiles = false
|
|
this.semaphore.release(semaphoreSymbol)
|
|
this.fetchSemaphore.release(fetchSemaphoreSymbol)
|
|
}
|
|
|
|
return []
|
|
},
|
|
|
|
redirectToNewName({ album }) {
|
|
this.showEditAlbumForm = false
|
|
|
|
if (this.album.basename !== album.basename) {
|
|
this.$router.push(`/albums/${album.basename}`)
|
|
}
|
|
},
|
|
|
|
async handleFilesPicked(fileIds) {
|
|
this.showAddPhotosModal = false
|
|
await this.addFilesToAlbum({ albumName: this.albumName, fileIdsToAdd: fileIds })
|
|
// Re-fetch album content to have the proper filenames.
|
|
await this.fetchAlbumContent()
|
|
},
|
|
|
|
async handleRemoveFilesFromAlbum(fileIds) {
|
|
this.$refs.collectionContent.onUncheckFiles(fileIds)
|
|
await this.removeFilesFromAlbum({ albumName: this.albumName, fileIdsToRemove: fileIds })
|
|
},
|
|
|
|
async handleDeleteAlbum() {
|
|
await this.deleteAlbum({ albumName: this.albumName })
|
|
this.$router.push('/albums')
|
|
},
|
|
|
|
async handleSetCollaborators(collaborators) {
|
|
try {
|
|
this.loadingAddCollaborators = true
|
|
this.showManageCollaboratorView = false
|
|
await this.updateAlbum({ albumName: this.albumName, properties: { collaborators } })
|
|
} catch (error) {
|
|
logger.error(error)
|
|
} finally {
|
|
this.loadingAddCollaborators = false
|
|
}
|
|
},
|
|
|
|
/**
|
|
* A new File has been uploaded, let's add it
|
|
*
|
|
* @param {Upload[]} uploads
|
|
*/
|
|
onUpload: debounce(function() {
|
|
this.fetchAlbumContent()
|
|
}, 500),
|
|
},
|
|
}
|
|
</script>
|
|
<style lang="scss" scoped>
|
|
.album {
|
|
&__title {
|
|
width: 100%;
|
|
}
|
|
|
|
&__name {
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
&__location {
|
|
margin-left: -4px;
|
|
display: flex;
|
|
color: var(--color-text-lighter);
|
|
}
|
|
}
|
|
</style>
|