Tagging listing #1 & merging folder/tag component

Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
This commit is contained in:
John Molakvoæ (skjnldsv) 2019-11-21 13:35:08 +01:00
parent 4174c7bea6
commit 68ed13a6ce
No known key found for this signature in database
GPG Key ID: 60C25B8C072916CF
12 changed files with 499 additions and 96 deletions

View File

@ -21,33 +21,10 @@
-->
<template>
<router-link :class="{'folder--clear': isEmpty}"
class="folder"
:to="to"
:aria-label="ariaLabel">
<transition name="fade">
<div v-show="loaded"
:class="`folder-content--grid-${fileList.length}`"
class="folder-content"
role="none">
<img v-for="file in fileList"
:key="file.fileid"
:src="generateImgSrc(file)"
alt=""
@load="loaded = true">
</div>
</transition>
<div
class="folder-name">
<span :class="[!isEmpty ? 'icon-white' : 'icon-dark', icon]"
class="folder-name__icon"
role="img" />
<p :id="ariaUuid" class="folder-name__name">
{{ basename }}
</p>
</div>
<div class="cover" role="none" />
</router-link>
<FolderTagPreview :id="fileid"
:name="basename"
:path="filename"
:file-list="fileList" />
</template>
<script>
@ -56,9 +33,14 @@ import { mapGetters } from 'vuex'
import getAlbumContent from '../services/AlbumContent'
import cancelableRequest from '../utils/CancelableRequest'
import FolderTagPreview from './FolderTagPreview'
export default {
name: 'Folder',
components: {
FolderTagPreview,
},
inheritAttrs: false,
props: {
@ -74,10 +56,6 @@ export default {
type: Number,
required: true,
},
icon: {
type: String,
default: 'icon-folder',
},
showShared: {
type: Boolean,
default: false,
@ -86,11 +64,13 @@ export default {
data() {
return {
loaded: false,
cancelRequest: () => {},
}
},
beforeDestroy() {
this.cancelRequest('Navigated away')
},
computed: {
// global lists
...mapGetters([
@ -105,38 +85,11 @@ export default {
fileList() {
return this.folderContent
? this.folderContent
.slice(0, 4) // only get the 4 first images
.map(id => this.files[id])
.filter(file => !!file)
.slice(0, 4) // only get the 4 first images
: []
},
// folder is empty
isEmpty() {
return this.fileList.length === 0
},
ariaUuid() {
return `folder-${this.fileid}`
},
ariaLabel() {
return t('photos', 'Open the "{name}" sub-directory', { name: this.basename })
},
/**
* We do not want encoded slashes when browsing by folder
* so we generate a new valid route object, get the final url back
* decode it and use it as a direct string, which vue-router
* does not encode afterwards
* @returns {string}
*/
to() {
const route = Object.assign({}, this.$route, {
// always remove first slash
params: { path: this.filename.substr(1) },
})
return decodeURIComponent(this.$router.resolve(route).resolved.path)
},
},
async created() {
@ -160,17 +113,6 @@ export default {
beforeDestroy() {
this.cancelRequest('Navigated away')
},
methods: {
generateImgSrc({ fileid, etag }) {
// use etag to force cache reload if file changed
return generateUrl(`/core/preview?fileId=${fileid}&x=${256}&y=${256}&a=true&v=${etag}`)
},
fetch() {
},
},
}
</script>

View File

@ -0,0 +1,237 @@
<!--
- @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
-
- @author John Molakvoæ <skjnldsv@protonmail.com>
-
- @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>
<router-link :class="{'folder--clear': isEmpty}"
class="folder"
:to="to"
:aria-label="ariaLabel">
<!-- Images preview -->
<transition name="fade">
<div v-show="loaded"
:class="`folder-content--grid-${fileList.length}`"
class="folder-content"
role="none">
<img v-for="file in fileList"
:key="file.fileid"
:src="generateImgSrc(file)"
alt=""
@load="loaded = true">
</div>
</transition>
<div
class="folder-name">
<span :class="[!isEmpty ? 'icon-white' : 'icon-dark', icon]"
class="folder-name__icon"
role="img" />
<p :id="ariaUuid" class="folder-name__name">
{{ name }}
</p>
</div>
<div class="cover" role="none" />
</router-link>
</template>
<script>
import { generateUrl } from '@nextcloud/router'
export default {
name: 'FolderTagPreview',
props: {
icon: {
type: String,
default: 'icon-folder',
},
id: {
type: Number,
required: true,
},
name: {
type: String,
required: true,
},
path: {
type: String,
required: true,
},
fileList: {
type: Array,
default: () => [],
},
},
data() {
return {
loaded: false,
}
},
computed: {
// folder is empty
isEmpty() {
return this.fileList.length === 0
},
ariaUuid() {
return `folder-${this.id}`
},
ariaLabel() {
return t('photos', 'Open the "{name}" sub-directory', { name: this.name })
},
/**
* We do not want encoded slashes when browsing by folder
* so we generate a new valid route object based on the
* current named route, get the final url back, decode it
* and use it as a direct string.
* Which vue-router does not encode afterwards!
* @returns {string}
*/
to() {
// always remove first slash, the router
// manage it automatically
const regex = /^\/?(.+)/i
const path = regex.exec(this.path)[1]
// apply to current route
const route = Object.assign({}, this.$route, {
params: { path },
})
// returning a string prevent vue-router to encode it again
return decodeURIComponent(this.$router.resolve(route).resolved.path)
},
},
methods: {
generateImgSrc({ fileid, etag }) {
// use etag to force cache reload if file changed
return generateUrl(`/core/preview?fileId=${fileid}&x=${256}&y=${256}&a=true&v=${etag}`)
},
},
}
</script>
<style lang="scss" scoped>
@import '../mixins/FileFolder.scss';
.folder-content {
position: absolute;
display: grid;
width: 100%;
height: 100%;
// folder layout if less than 4 pictures
&--grid-1 {
grid-template-columns: 1fr;
grid-template-rows: 1fr;
}
&--grid-2 {
grid-template-columns: 1fr;
grid-template-rows: 1fr 1fr;
}
&--grid-3 {
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
img:first-child {
grid-column: span 2;
}
}
&--grid-4 {
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
}
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
$name-height: 1.2rem;
.folder-name {
position: absolute;
z-index: 3;
display: flex;
overflow: hidden;
flex-direction: column;
width: 100%;
height: 100%;
transition: opacity var(--animation-quick) ease-in-out;
opacity: 1;
&__icon {
height: 40%;
margin-top: calc(30% - #{$name-height} / 2); // center name+icon
background-size: 40%;
}
&__name {
overflow: hidden;
height: $name-height;
padding: 0 10px;
text-align: center;
white-space: nowrap;
text-overflow: ellipsis;
color: var(--color-main-background);
text-shadow: 0 0 8px var(--color-main-text);
font-size: $name-height;
line-height: $name-height;
}
}
// Cover management empty/full
.folder {
// if no img, let's display the folder icon as default black
&--clear {
.folder-name__icon {
opacity: .3;
}
.folder-name__name {
color: var(--color-main-text);
text-shadow: 0 0 8px var(--color-main-background);
}
}
// show the cover as background
// if there are pictures in it
// so we can sho the folder+name above it
&:not(.folder--clear) {
.cover {
opacity: .3;
}
// hide everything but pictures
// on hover/active/focus
&:active,
&:hover,
&:focus {
.folder-name,
.cover {
opacity: 0;
}
}
}
}
</style>

106
src/components/Tag.vue Normal file
View File

@ -0,0 +1,106 @@
<!--
- @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
-
- @author John Molakvoæ <skjnldsv@protonmail.com>
-
- @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>
<FolderTagPreview :id="id"
icon="icon-tag"
:name="displayName"
:path="displayName"
:file-list="fileList" />
</template>
<script>
import { mapGetters } from 'vuex'
import getTaggedImages from '../services/TaggedImages'
import cancelableRequest from '../utils/CancelableRequest'
import FolderTagPreview from './FolderTagPreview'
export default {
name: 'Tag',
components: {
FolderTagPreview,
},
inheritAttrs: false,
props: {
displayName: {
type: String,
required: true,
},
id: {
type: Number,
required: true,
},
},
data() {
return {
cancelRequest: () => {},
}
},
beforeDestroy() {
this.cancelRequest('Navigated away')
},
computed: {
// global lists
...mapGetters([
'files',
'tags',
]),
// files list of the current folder
folderContent() {
return this.tags[this.id].files
},
fileList() {
return this.folderContent
? this.folderContent
.map(id => this.files[id])
.filter(file => !!file)
.slice(0, 4) // only get the 4 first images
: []
},
},
async created() {
// init cancellable request
const { request, cancel } = cancelableRequest(getTaggedImages)
this.cancelRequest = cancel
try {
// get data
const files = await request(this.id, { shared: this.showShared })
this.$store.dispatch('updateTag', { id: this.id, files })
this.$store.dispatch('appendFiles', files)
} catch (error) {
if (error.response && error.response.status) {
console.error('Failed to get folder content', this.id, error.response)
}
// else we just cancelled the request
}
},
}
</script>

View File

@ -33,8 +33,14 @@ request.prepareRequestOptions = function(requestOptions, methodOptions) {
if (methodOptions.cancelToken && typeof methodOptions.cancelToken === 'object') {
requestOptions.cancelToken = Object.assign({}, requestOptions.cancelToken || {}, methodOptions.cancelToken)
}
// exploit old method
oldPrepareRequestOptions(requestOptions, methodOptions)
// allow us to override the request method
if (methodOptions.method && typeof methodOptions.method === 'string') {
requestOptions.method = methodOptions.method
}
}
module.exports = request

View File

@ -38,14 +38,13 @@ export default async function(path = '/', options = {}) {
// fetch listing
const response = await axios.get(prefixPath + path, options)
const list = response.data.map(data => genFileInfo(data, prefixPath))
// filter all the files and folders
let folder = {}
const folders = []
const files = []
console.info(allowedMimes)
for (const entry of list) {
// is this the current provided path ?
if (entry.filename === path) {

View File

@ -70,7 +70,7 @@ export default async function(path, options) {
.then(result => getDirectoryFiles(result, remotePath + prefixPath, options.details))
.then(files => processResponsePayload(response, files, options.details))
const list = data.map(data => genFileInfo(data, prefixPath))
const list = data.map(data => genFileInfo(data))
// filter all the files and folders
let folder = {}

View File

@ -0,0 +1,99 @@
/**
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @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/>.
*
*/
/**
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @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/>.
*
*/
import { genFileInfo } from '../utils/fileUtils'
import { getCurrentUser } from '@nextcloud/auth'
import client from './DavClient'
/**
* Get tagged files based on provided tag id
*
* @param {number} id the tag id to filter
* @param {Object} [options] optional options for axios
* @returns {Array} the file list
*/
export default async function(id, options = {}) {
const prefixPath = `/files/${getCurrentUser().uid}`
const response = await client.getDirectoryContents(prefixPath, Object.assign({}, {
method: 'REPORT',
data: `<?xml version="1.0"?>
<oc:filter-files
xmlns:d="DAV:"
xmlns:oc="http://owncloud.org/ns"
xmlns:nc="http://nextcloud.org/ns"
xmlns:ocs="http://open-collaboration-services.org/ns">
<d:prop>
<d:getlastmodified />
<d:getetag />
<d:getcontenttype />
<d:resourcetype />
<oc:fileid />
<oc:permissions />
<oc:size />
<d:getcontentlength />
<nc:has-preview />
<nc:mount-type />
<nc:is-encrypted />
<ocs:share-permissions />
<oc:tags />
<oc:favorite />
<oc:comments-unread />
<oc:owner-id />
<oc:owner-display-name />
<oc:share-types />
</d:prop>
<oc:filter-rules>
<oc:systemtag>${id}</oc:systemtag>
</oc:filter-rules>
</oc:filter-files>`,
details: true,
}, options))
return response.data
.map(data => genFileInfo(data, prefixPath))
// remove prefix path from full file path
.map(data => Object.assign({}, data, { filename: data.filename.replace(prefixPath, '') }))
}

View File

@ -52,6 +52,7 @@ const mutations = {
if (state.files[fileid]) {
const subfolders = folders
.map(folder => folder.fileid)
// some invalid folders have an id of -1 (ext storage)
.filter(id => id >= 0)
Vue.set(state.files[fileid], 'folders', subfolders)
}
@ -64,7 +65,7 @@ const getters = {
const actions = {
/**
* Increment the number of contacts accepted
* Update files, folders and their respective subfolders
*
* @param {Object} context the store mutations
* @param {Object} data destructuring object
@ -72,11 +73,21 @@ const actions = {
* @param {Array} data.files list of files
* @param {Array} data.folders list of folders within current folder
*/
updateFiles(context, { folder, files, folders }) {
updateFiles(context, { folder, files = [], folders = [] } = {}) {
// we want all the FileInfo! Folders included!
context.commit('updateFiles', [folder, ...files, ...folders])
context.commit('setSubFolders', { fileid: folder.fileid, folders })
},
/**
* Append or update given files
*
* @param {Object} context the store mutations
* @param {Array} files list of files
*/
appendFiles(context, files = []) {
context.commit('updateFiles', files)
},
}
export default { state, mutations, getters, actions }

View File

@ -61,7 +61,8 @@ const mutations = {
const list = files.sort((a, b) => sortCompare(a, b, 'lastmod'))
// overwrite list
Vue.set(state.tags[id], 'files', list.map(file => file.id))
console.info(id, list)
Vue.set(state.tags[id], 'files', list.map(file => file.fileid))
},
}

View File

@ -29,12 +29,13 @@
{{ t('photos', 'An error occurred') }}
</EmptyContent>
<EmptyContent v-else-if="!loading && isEmpty" illustration-name="empty">
{{ t('photos', 'This folder does not contain pictures') }}
{{ t('photos', 'No photos in here') }}
</EmptyContent>
<!-- Folder content -->
<Grid v-else>
<Navigation v-if="folder" key="navigation" v-bind="folder" />
<Folder v-for="dir in folderList"
:key="dir.fileid"
v-bind="dir"
@ -182,7 +183,7 @@ export default {
if (error.response.status === 404) {
this.error = 404
setTimeout(() => {
this.$router.push({ name: 'root' })
this.$router.push({ name: this.$route.name })
}, 3000)
} else {
this.error = error

View File

@ -25,6 +25,9 @@
<div v-if="haveIllustration" class="illustration" v-html="illustration" />
<div v-else class="icon-error" />
<h2><slot /></h2>
<p v-show="$slots.desc">
<slot name="desc" />
</p>
</div>
</template>

View File

@ -22,35 +22,29 @@
<template>
<!-- Errors handlers-->
<!-- <EmptyContent v-if="error === 404" illustration-name="folder">
{{ t('photos', 'This folder does not exists') }}
</EmptyContent>
<EmptyContent v-else-if="error">
<EmptyContent v-if="error">
{{ t('photos', 'An error occurred') }}
</EmptyContent>
<EmptyContent v-else-if="!loading && isEmpty" illustration-name="empty">
{{ t('photos', 'This folder does not contain pictures') }}
</EmptyContent> -->
{{ t('photos', 'No tags yet') }}
<template #desc>
{{ t('photos', 'Photos with tags will show up here') }}
</template>
</EmptyContent>
<!-- Folder content -->
<Grid v-if="isRoot">
<Grid v-else-if="isRoot">
<Navigation v-if="tag"
key="navigation"
:basename="tagname"
:filename="'/' + tagname"
:root-title="t('photos', 'Tags')" />
<Folder v-for="id in tagsNames"
<Tag v-for="id in tagsNames"
:key="id"
v-bind="tags[id]"
:fileid="id"
:basename="tags[id].displayName"
icon="icon-tag" />
:basename="tags[id].displayName" />
</Grid>
<!-- <Grid v-else>
<Navigation v-if="folder" key="navigation" v-bind="folder" />
<Folder v-for="dir in folderList" :key="dir.id" :folder="dir" />
<File v-for="file in fileList" :key="file.id" v-bind="file" />
</Grid> -->
</template>
<script>
@ -59,7 +53,7 @@ import { mapGetters } from 'vuex'
import getSystemTags from '../services/SystemTags'
import EmptyContent from './EmptyContent'
import Folder from '../components/Folder'
import Tag from '../components/Tag'
import File from '../components/File'
import Grid from '../components/Grid'
import Navigation from '../components/Navigation'
@ -71,7 +65,7 @@ export default {
components: {
EmptyContent,
File,
Folder,
Tag,
Grid,
Navigation,
},
@ -120,6 +114,10 @@ export default {
isRoot() {
return this.tagname === ''
},
isEmpty() {
return Object.keys(this.tagsNames).length === 0
},
},
watch: {