mirror of https://github.com/nextcloud/photos
306 lines
7.6 KiB
Vue
306 lines
7.6 KiB
Vue
<!--
|
|
- @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
|
|
-
|
|
- @author John Molakvoæ <skjnldsv@protonmail.com>
|
|
- @author Corentin Mors <medias@pixelswap.fr>
|
|
-
|
|
- @license GNU AGPL version 3 or any later version
|
|
-
|
|
- 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>
|
|
<!-- Errors handlers-->
|
|
<EmptyContent v-if="error === 404" illustration-name="folder">
|
|
{{ t('photos', 'This folder does not exist') }}
|
|
</EmptyContent>
|
|
<EmptyContent v-else-if="error">
|
|
{{ t('photos', 'An error occurred') }}
|
|
</EmptyContent>
|
|
|
|
<!-- Folder content -->
|
|
<div v-else-if="!loading">
|
|
<Navigation
|
|
v-if="isEmpty"
|
|
key="navigation"
|
|
:basename="path"
|
|
:filename="'/'"
|
|
:root-title="rootTitle" />
|
|
|
|
<EmptyContent v-if="isEmpty" illustration-name="empty">
|
|
{{ t('photos', 'No photos in here') }}
|
|
</EmptyContent>
|
|
|
|
<div class="grid-container">
|
|
<VirtualGrid
|
|
ref="virtualgrid"
|
|
:items="contentList"
|
|
:update-function="getContent"
|
|
:get-column-count="() => gridConfig.count"
|
|
:get-grid-gap="() => gridConfig.gap"
|
|
:update-trigger-margin="700"
|
|
:loader="loaderComponent" />
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import moment from '@nextcloud/moment'
|
|
import { mapGetters } from 'vuex'
|
|
|
|
import getPhotos from '../services/PhotoSearch'
|
|
|
|
import EmptyContent from '../components/EmptyContent'
|
|
import File from '../components/File'
|
|
import SeparatorVirtualGrid from '../components/SeparatorVirtualGrid'
|
|
import VirtualGrid from 'vue-virtual-grid'
|
|
import Navigation from '../components/Navigation'
|
|
import Loader from '../components/Loader'
|
|
|
|
import cancelableRequest from '../utils/CancelableRequest'
|
|
import GridConfigMixin from '../mixins/GridConfig'
|
|
import { allMimes } from '../services/AllowedMimes'
|
|
|
|
export default {
|
|
name: 'Timeline',
|
|
components: {
|
|
EmptyContent,
|
|
VirtualGrid,
|
|
Navigation,
|
|
},
|
|
mixins: [GridConfigMixin],
|
|
props: {
|
|
loading: {
|
|
type: Boolean,
|
|
required: true,
|
|
},
|
|
onlyFavorites: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
mimesType: {
|
|
type: Array,
|
|
default: () => allMimes,
|
|
},
|
|
rootTitle: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
path: {
|
|
type: String,
|
|
default: '',
|
|
},
|
|
},
|
|
|
|
data() {
|
|
return {
|
|
cancelRequest: null,
|
|
done: false,
|
|
error: null,
|
|
page: 0,
|
|
lastSection: '',
|
|
loaderComponent: Loader,
|
|
}
|
|
},
|
|
|
|
computed: {
|
|
// global lists
|
|
...mapGetters([
|
|
'files',
|
|
'timeline',
|
|
]),
|
|
// list of loaded medias
|
|
fileList() {
|
|
return this.timeline.map((fileId) => this.files[fileId])
|
|
},
|
|
// list of displayed content in the grid (titles + medias)
|
|
contentList() {
|
|
/** The goal of this flat map is to return an array of images separated by titles (months)
|
|
* ie: [{month1}, {image1}, {image2}, {month2}, {image3}, {image4}, {image5}]
|
|
* First we get the current month+year of the image
|
|
* We compare it to the previous image month+year
|
|
* If there is a difference we have to insert a title object before the current image
|
|
* If it's equal we just add the current image to the array
|
|
* Note: the injected param of objects are used to pass custom params to the grid lib
|
|
* In our case injected could be an image/video (aka file) or a title (year/month)
|
|
* Note2: titles are rendered full width and images are rendered on 1 column and 256x256 ratio
|
|
*/
|
|
return this.fileList.flatMap((file, index) => {
|
|
const finalArray = []
|
|
const currentSection = this.getFormatedDate(file.lastmod, 'YYYY MMMM')
|
|
if (this.lastSection !== currentSection) {
|
|
finalArray.push({
|
|
id: `title-${index}`,
|
|
injected: {
|
|
year: this.getFormatedDate(file.lastmod, 'YYYY'),
|
|
month: this.getFormatedDate(file.lastmod, 'MMMM'),
|
|
},
|
|
height: 90,
|
|
columnSpan: 0, // means full width
|
|
newRow: true,
|
|
renderComponent: SeparatorVirtualGrid,
|
|
})
|
|
this.lastSection = currentSection // we keep track of the last section for the next batch
|
|
}
|
|
finalArray.push({
|
|
id: `img-${file.fileid}`,
|
|
injected: {
|
|
...file,
|
|
list: this.fileList,
|
|
loadMore: this.getContent,
|
|
canLoop: false,
|
|
},
|
|
width: 256,
|
|
height: 256,
|
|
columnSpan: 1,
|
|
renderComponent: File,
|
|
})
|
|
return finalArray
|
|
})
|
|
},
|
|
// is current folder empty?
|
|
isEmpty() {
|
|
return this.fileList.length === 0
|
|
},
|
|
},
|
|
|
|
watch: {
|
|
async onlyFavorites() {
|
|
// reset component
|
|
this.resetState()
|
|
this.getContent()
|
|
},
|
|
async mimesType() {
|
|
// reset component
|
|
this.resetState()
|
|
this.getContent()
|
|
},
|
|
},
|
|
|
|
beforeMount() {
|
|
this.getContent()
|
|
},
|
|
|
|
beforeDestroy() {
|
|
// cancel any pending requests
|
|
if (this.cancelRequest) {
|
|
this.cancelRequest('Changed view')
|
|
}
|
|
this.resetState()
|
|
},
|
|
|
|
methods: {
|
|
/** Return next batch of data depending on global offset
|
|
* @param {boolean} doReturn Returns a Promise with the list instead of a boolean
|
|
* @returns {Promise<boolean>} Returns a Promise with a boolean that stops infinite loading
|
|
*/
|
|
async getContent(doReturn) {
|
|
if (this.done) {
|
|
return Promise.resolve(true)
|
|
}
|
|
|
|
// cancel any pending requests
|
|
if (this.cancelRequest) {
|
|
this.cancelRequest('Changed view')
|
|
}
|
|
|
|
// if we don't already have some cached data let's show a loader
|
|
if (this.timeline.length === 0) {
|
|
this.$emit('update:loading', true)
|
|
}
|
|
|
|
// done loading even with errors
|
|
const { request, cancel } = cancelableRequest(getPhotos)
|
|
this.cancelRequest = cancel
|
|
|
|
const numberOfImagesPerBatch = this.gridConfig.count * 5 // loading 5 rows
|
|
|
|
try {
|
|
// Load next batch of images
|
|
const files = await request(this.onlyFavorites, {
|
|
page: this.page,
|
|
perPage: numberOfImagesPerBatch,
|
|
mimesType: this.mimesType,
|
|
})
|
|
|
|
// If we get less files than requested that means we got to the end
|
|
if (files.length !== numberOfImagesPerBatch) {
|
|
this.done = true
|
|
}
|
|
|
|
this.$store.dispatch('updateTimeline', files)
|
|
this.$store.dispatch('appendFiles', files)
|
|
|
|
this.page += 1
|
|
|
|
if (doReturn) {
|
|
return Promise.resolve(files)
|
|
}
|
|
|
|
return Promise.resolve(false)
|
|
} catch (error) {
|
|
if (error.response && error.response.status) {
|
|
if (error.response.status === 404) {
|
|
this.error = 404
|
|
setTimeout(() => {
|
|
this.$router.push({ name: this.$route.name })
|
|
}, 3000)
|
|
} else {
|
|
this.error = error
|
|
}
|
|
}
|
|
|
|
// cancelled request, moving on...
|
|
console.error('Error fetching timeline', error)
|
|
return Promise.resolve(true)
|
|
} finally {
|
|
// done loading even with errors
|
|
this.$emit('update:loading', false)
|
|
this.cancelRequest = null
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Reset this component data to a pristine state
|
|
*/
|
|
resetState() {
|
|
this.$store.dispatch('resetTimeline')
|
|
this.done = false
|
|
this.error = null
|
|
this.page = 0
|
|
this.lastSection = ''
|
|
this.$emit('update:loading', true)
|
|
this.$refs.virtualgrid.resetGrid()
|
|
},
|
|
|
|
getFormatedDate(string, format) {
|
|
return moment(string).format(format)
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
@import '../mixins/GridSizes.scss';
|
|
|
|
.grid-container {
|
|
@include grid-sizes using ($marginTop, $marginW) {
|
|
padding: 0px #{$marginW}px 256px #{$marginW}px;
|
|
}
|
|
}
|
|
</style>
|