From 2366aaf6ad5d607762f9453884bfb600f13b2c93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20Molakvo=C3=A6=20=28skjnldsv=29?= Date: Tue, 12 Nov 2019 07:12:36 +0100 Subject: [PATCH] =?UTF-8?q?Tags=20Signed-off-by:=20John=20Molakvo=C3=A6=20?= =?UTF-8?q?(skjnldsv)=20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- css/icons.scss | 2 +- img/photos.svg | 2 +- package-lock.json | 3 +- package.json | 1 + src/Photos.vue | 2 +- src/components/Folder.vue | 31 ++++-- src/components/Navigation.vue | 13 ++- src/patchedRequest.js | 3 +- src/router/index.js | 20 ++-- src/services/FileList.js | 8 +- src/services/FolderInfo.js | 4 +- src/services/PhotoSearch.js | 2 +- src/services/SystemTags.js | 22 ++-- src/store/folders.js | 5 +- src/store/systemtags.js | 66 ++++++------ src/utils/fileUtils.js | 125 ++++++++++++++++++++++ src/utils/{ParseFile.js => numberUtil.js} | 21 ++-- src/views/Albums.vue | 2 +- src/views/Tags.vue | 71 ++++++++++-- webpack.common.js | 1 + 20 files changed, 288 insertions(+), 116 deletions(-) create mode 100644 src/utils/fileUtils.js rename src/utils/{ParseFile.js => numberUtil.js} (61%) diff --git a/css/icons.scss b/css/icons.scss index 88e42707..ba8dcef9 100644 --- a/css/icons.scss +++ b/css/icons.scss @@ -24,4 +24,4 @@ @include icon-color('folder', 'filetypes', $color-black, 1, true); } -@include icon-black-white('photos', 'photos', 1); +@include icon-black-white('photos', 'photos', 2); diff --git a/img/photos.svg b/img/photos.svg index 90c3fdb6..6046f92b 100644 --- a/img/photos.svg +++ b/img/photos.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 39a3b402..7a8031b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1853,8 +1853,7 @@ "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" }, "camelcase-keys": { "version": "2.1.0", diff --git a/package.json b/package.json index abc6819f..4ee8bf4a 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@nextcloud/l10n": "^0.2.1", "@nextcloud/router": "^0.1.0", "@nextcloud/vue": "^1.1.0", + "camelcase": "^5.3.1", "cdav-library": "git+https://github.com/nextcloud/cdav-library.git", "path-posix": "^1.0.0", "qs": "^6.9.0", diff --git a/src/Photos.vue b/src/Photos.vue index ec26f323..49b83e13 100644 --- a/src/Photos.vue +++ b/src/Photos.vue @@ -30,7 +30,7 @@ - + diff --git a/src/components/Folder.vue b/src/components/Folder.vue index de2db5a6..c04e17a9 100644 --- a/src/components/Folder.vue +++ b/src/components/Folder.vue @@ -43,7 +43,7 @@ class="folder-name__icon" role="img" />

- {{ folder.basename }} + {{ basename }}

@@ -62,8 +62,16 @@ export default { inheritAttrs: false, props: { - folder: { - type: Object, + basename: { + type: String, + required: true, + }, + filename: { + type: String, + required: true, + }, + id: { + type: Number, required: true, }, icon: { @@ -88,7 +96,7 @@ export default { // files list of the current folder folderContent() { - return this.folders[this.folder.id] + return this.folders[this.id] }, fileList() { return this.folderContent @@ -105,23 +113,24 @@ export default { }, ariaUuid() { - return `folder-${this.folder.id}` + return `folder-${this.id}` }, ariaLabel() { - return t('photos', 'Open the "{name}" sub-directory', { name: this.folder.basename }) + 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 + * 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.folder.filename.substr(1) } - }); + params: { path: this.filename.substr(1) }, + }) return decodeURIComponent(this.$router.resolve(route).resolved.path) }, }, @@ -133,9 +142,9 @@ export default { try { // get data - const { files, folders } = await request(this.folder.filename) + const { files, folders } = await request(this.filename) // this.cancelRequest('Stop!') - this.$store.dispatch('updateFolders', { id: this.folder.id, files, folders }) + this.$store.dispatch('updateFolders', { id: this.id, files, folders }) this.$store.dispatch('updateFiles', { folder: this.folder, files, folders }) } catch (error) { if (error.response && error.response.status) { diff --git a/src/components/Navigation.vue b/src/components/Navigation.vue index df1c3f0f..a0d4c92c 100644 --- a/src/components/Navigation.vue +++ b/src/components/Navigation.vue @@ -56,6 +56,10 @@ export default { type: String, required: true, }, + rootTitle: { + type: String, + default: t('photos', 'Photos'), + }, id: { type: Number, required: true, @@ -68,7 +72,7 @@ export default { }, name() { if (this.isRoot) { - return t('photos', 'Photos') + return this.rootTitle } return this.basename }, @@ -93,14 +97,15 @@ export default { /** * 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 + * 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.parentPath.substr(1) } - }); + params: { path: this.parentPath.substr(1) }, + }) return decodeURIComponent(this.$router.resolve(route).resolved.path) }, }, diff --git a/src/patchedRequest.js b/src/patchedRequest.js index 6b1eb267..ae23b19d 100644 --- a/src/patchedRequest.js +++ b/src/patchedRequest.js @@ -21,7 +21,6 @@ */ const request = require('webdav/dist/request') -const merge = require('webdav/dist/merge') const oldPrepareRequestOptions = request.prepareRequestOptions @@ -32,7 +31,7 @@ const oldPrepareRequestOptions = request.prepareRequestOptions request.prepareRequestOptions = function(requestOptions, methodOptions) { // add our cancelToken support if (methodOptions.cancelToken && typeof methodOptions.cancelToken === 'object') { - requestOptions.cancelToken = merge(requestOptions.cancelToken || {}, methodOptions.cancelToken) + requestOptions.cancelToken = Object.assign({}, requestOptions.cancelToken || {}, methodOptions.cancelToken) } // exploit old method oldPrepareRequestOptions(requestOptions, methodOptions) diff --git a/src/router/index.js b/src/router/index.js index df464138..d4d9caa6 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -55,7 +55,7 @@ export default new Router({ children: [ { path: ':path*', - name: 'path', + name: 'albumspath', component: Albums, }, ], @@ -68,7 +68,7 @@ export default new Router({ children: [ { path: ':path*', - name: 'path', + name: 'sharedpath', component: Albums, }, ], @@ -77,24 +77,18 @@ export default new Router({ path: '/favorites', component: Tags, name: 'favorites', - props, - children: [ - { - path: ':path*', - name: 'path', - component: Tags, - }, - ], }, { path: '/tags', component: Tags, name: 'tags', - props, + props: route => ({ + tagname: route.params.tagname, + }), children: [ { - path: ':path*', - name: 'path', + path: ':tagname', + name: 'tagname', component: Tags, }, ], diff --git a/src/services/FileList.js b/src/services/FileList.js index 80e9df6d..c473aad8 100644 --- a/src/services/FileList.js +++ b/src/services/FileList.js @@ -26,8 +26,7 @@ import { handleResponseCode, processResponsePayload } from 'webdav/dist/response import { normaliseHREF, normalisePath } from 'webdav/dist/url' import client, { remotePath } from './DavClient' import pathPosix from 'path-posix' -import request from './DavRequest' -import parseFile from '../utils/ParseFile' +import { genFileInfo } from '../utils/fileUtils' /** * List files from a folder and filter out unwanted mimes @@ -37,6 +36,8 @@ import parseFile from '../utils/ParseFile' * @returns {Array} the file list */ export default async function(path, options) { + + console.trace(); options = Object.assign({ method: 'PROPFIND', headers: { @@ -44,7 +45,6 @@ export default async function(path, options) { Depth: options.deep ? 'infinity' : 1, }, responseType: 'text', - data: request, details: true, }, options) @@ -68,7 +68,7 @@ export default async function(path, options) { .then(result => getDirectoryFiles(result, remotePath, options.details)) .then(files => processResponsePayload(response, files, options.details)) - const list = data.map(data => parseFile(data, prefixPath)) + const list = data.map(data => genFileInfo(data, prefixPath)) // filter all the files and folders let folder = {} diff --git a/src/services/FolderInfo.js b/src/services/FolderInfo.js index 6d65b57a..b58ddb79 100644 --- a/src/services/FolderInfo.js +++ b/src/services/FolderInfo.js @@ -23,7 +23,7 @@ import { getCurrentUser } from '@nextcloud/auth' import client from './DavClient' import request from './DavRequest' -import parseFile from '../utils/ParseFile' +import { genFileInfo } from '../utils/fileUtils' /** * List files from a folder and filter out unwanted mimes @@ -43,5 +43,5 @@ export default async function(path) { details: true, }) - return parseFile(response.data, prefixPath) + return genFileInfo(response.data, prefixPath) } diff --git a/src/services/PhotoSearch.js b/src/services/PhotoSearch.js index e4546bb4..8d3789ea 100644 --- a/src/services/PhotoSearch.js +++ b/src/services/PhotoSearch.js @@ -23,7 +23,7 @@ import { generateRemoteUrl } from '@nextcloud/router' import { getCurrentUser } from '@nextcloud/auth' import client from './DavClient' -import parseFile from '../utils/ParseFile' +import { genFileInfo } from '../utils/fileUtils' /** * List files from a folder and filter out unwanted mimes diff --git a/src/services/SystemTags.js b/src/services/SystemTags.js index a8ce55eb..c0244a31 100644 --- a/src/services/SystemTags.js +++ b/src/services/SystemTags.js @@ -21,15 +21,17 @@ */ import client from './DavClient' -import { generateRemoteUrl } from '@nextcloud/router' +import { genFileInfo } from '../utils/fileUtils' /** - * List files from a folder and filter out unwanted mimes + * List system tags * + * @param {String} path the path relative to the user root + * @param {Object} [options] optional options for axios * @returns {Array} the file list */ -export default async function() { - const response = await client.getDirectoryContents('/systemtags/', { +export default async function(path, options = {}) { + const response = await client.getDirectoryContents('/systemtags/', Object.assign({}, { data: ` @@ -42,15 +44,7 @@ export default async function() { `, details: true, - }) - - console.info(response) - - const entry = response.data - return Object.assign({ - id: parseInt(entry.props.fileid), - isFavorite: entry.props.favorite !== '0', - hasPreview: entry.props['has-preview'] !== 'false', - }, entry) + }, options)) + return response.data.map(data => genFileInfo(data)) } diff --git a/src/store/folders.js b/src/store/folders.js index 559d3ef2..1075900d 100644 --- a/src/store/folders.js +++ b/src/store/folders.js @@ -20,6 +20,7 @@ * */ import Vue from 'vue' +import { sortCompare } from '../utils/fileUtils' const state = { paths: {}, @@ -39,9 +40,7 @@ const mutations = { if (files.length > 0) { const t0 = performance.now() // sort by last modified - const list = files.sort((a, b) => { - return new Date(b.lastmod).getTime() - new Date(a.lastmod).getTime() - }) + const list = files.sort((a, b) => sortCompare(a, b, 'lastmod')) // Set folder list Vue.set(state.folders, id, list.map(file => file.id)) diff --git a/src/store/systemtags.js b/src/store/systemtags.js index 8f213e9e..4a49e13e 100644 --- a/src/store/systemtags.js +++ b/src/store/systemtags.js @@ -20,50 +20,56 @@ * */ import Vue from 'vue' +import { sortCompare } from '../utils/fileUtils' const state = { - paths: {}, tags: {}, + names: {}, } const mutations = { /** - * Index folders paths and ids + * Order and save tags * * @param {Object} state vuex state - * @param {Object} data destructuring object - * @param {number} data.id current folder id - * @param {Array} data.files list of files + * @param {Array} tags the tags list */ - updateTags(state, { id, files }) { - if (files.length > 0) { - // sort by last modified - const list = files.sort((a, b) => { - return new Date(b.lastmod).getTime() - new Date(a.lastmod).getTime() - }) + updateTags(state, tags) { + if (tags.length > 0) { + // sort by basename + const list = tags.sort((a, b) => sortCompare(a, b, 'displayName')) - // Set folder list - Vue.set(state.tags, id, list.map(file => file.id)) + // store tag and its index + list.forEach(tag => { + Vue.set(state.tags, tag.id, tag) + Vue.set(state.tags[tag.id], 'files', []) + Vue.set(state.names, tag.displayName, tag.id) + }) } }, /** - * Index folders paths and ids + * Update tag files list * * @param {Object} state vuex state * @param {Object} data destructuring object - * @param {string} data.path path of this folder - * @param {number} data.id id of this folder + * @param {number} data.id current tag id + * @param {Object[]} data.files list of files */ - addPath(state, { path, id }) { - Vue.set(state.paths, path, id) + updateTag(state, { id, files }) { + // sort by last modified + const list = files.sort((a, b) => sortCompare(a, b, 'lastmod')) + + // overwrite list + Vue.set(state.tags[id], 'files', list.map(file => file.id)) }, } const getters = { tags: state => state.tags, + tagsNames: state => state.names, tag: state => id => state.tags[id], - tagId: state => path => state.paths[path], + tagId: state => name => state.names[name], } const actions = { @@ -71,28 +77,22 @@ const actions = { * Update files and folders * * @param {Object} context vuex context - * @param {Object} data destructuring object - * @param {number} data.id current folder id - * @param {Array} data.files list of files - * @param {Array} data.folders list of folders + * @param {Array} tags the tag list */ - updateTags(context, { id, files, folders }) { - context.commit('updateTags', { id, files }) - - // then add each folders path indexes - folders.forEach(folder => context.commit('addPath', { path: folder.filename, id: folder.id })) + updateTags(context, tags) { + context.commit('updateTags', tags) }, /** - * Index folders paths and ids + * Update tag files list * * @param {Object} context vuex context * @param {Object} data destructuring object - * @param {string} data.path path of this folder - * @param {number} data.id id of this folder + * @param {number} data.id current tag id + * @param {Object[]} data.files list of files */ - addPath(context, { path, id }) { - context.commit('addPath', { path, id }) + updateTag(context, { id, files }) { + context.commit('updateTag', { id, files }) }, } diff --git a/src/utils/fileUtils.js b/src/utils/fileUtils.js new file mode 100644 index 00000000..ce18c08f --- /dev/null +++ b/src/utils/fileUtils.js @@ -0,0 +1,125 @@ +/** + * @copyright Copyright (c) 2019 John Molakvoæ + * + * @author John Molakvoæ + * + * @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 . + * + */ +import camelcase from 'camelcase' +import { isNumber } from './numberUtil' + +/** + * Get an url encoded path + * + * @param {String} path the full path + * @returns {string} url encoded file path + */ +const encodeFilePath = function(path) { + const pathSections = (path.startsWith('/') ? path : `/${path}`).split('/') + let relativePath = '' + pathSections.forEach((section) => { + if (section !== '') { + relativePath += '/' + encodeURIComponent(section) + } + }) + return relativePath +} + +/** + * Extract dir and name from file path + * + * @param {String} path the full path + * @returns {String[]} [dirPath, fileName] + */ +const extractFilePaths = function(path) { + const pathSections = path.split('/') + const fileName = pathSections[pathSections.length - 1] + const dirPath = pathSections.slice(0, pathSections.length - 1).join('/') + return [dirPath, fileName] +} + +/** + * Sorting comparison function + * + * @param {Object} fileInfo1 file 1 fileinfo + * @param {Object} fileInfo2 file 2 fileinfo + * @param {string} key key to sort with + * @param {boolean} [asc=true] sort ascending? + * @returns {number} + */ +const sortCompare = function(fileInfo1, fileInfo2, key, asc = true) { + + // favorite always first + if (fileInfo1.isFavorite && !fileInfo2.isFavorite) { + return -1 + } else if (!fileInfo1.isFavorite && fileInfo2.isFavorite) { + return 1 + } + + // if this is a number, let's sort by integer + if (isNumber(fileInfo1[key]) && isNumber(fileInfo2[key])) { + return asc + ? Number(fileInfo2[key]) - Number(fileInfo1[key]) + : Number(fileInfo1[key]) - Number(fileInfo2[key]) + } + + // else we sort by string, so let's sort directories first + if (fileInfo1.type === 'directory' && fileInfo2.type !== 'directory') { + return asc ? -1 : 1 + } else if (fileInfo1.type !== 'directory' && fileInfo2.type === 'directory') { + return asc ? 1 : -1 + } + + // if this is a date, let's sort by date + if (isNumber(new Date(fileInfo1[key]).getTime()) && isNumber(new Date(fileInfo2[key])).getTime()) { + return asc + ? new Date(fileInfo2[key]).getTime() - new Date(fileInfo1[key]).getTime() + : new Date(fileInfo1[key]).getTime() - new Date(fileInfo2[key]).getTime() + } + + // finally sort by name + return asc + ? fileInfo1[key].localeCompare(fileInfo2[key], OC.getLanguage()) + : -fileInfo1[key].localeCompare(fileInfo2[key], OC.getLanguage()) +} + +const genFileInfo = function(obj) { + const fileInfo = {} + + Object.keys(obj).forEach(key => { + const data = obj[key] + + // flatten object if any + if (!!data && typeof data === 'object') { + Object.assign(fileInfo, genFileInfo(data)) + } else { + // format key and add it to the fileInfo + if (data === 'false') { + fileInfo[camelcase(key)] = false + } else if (data === 'true') { + fileInfo[camelcase(key)] = true + } else { + fileInfo[camelcase(key)] = isNumber(data) + ? Number(data) + : data + } + } + }) + return fileInfo +} + +export { encodeFilePath, extractFilePaths, sortCompare, genFileInfo } diff --git a/src/utils/ParseFile.js b/src/utils/numberUtil.js similarity index 61% rename from src/utils/ParseFile.js rename to src/utils/numberUtil.js index 8d9d9baf..0c3a96e5 100644 --- a/src/utils/ParseFile.js +++ b/src/utils/numberUtil.js @@ -20,18 +20,11 @@ * */ -/** - * Format a file into a usable fileinfo object - * - * @param {Object} fileData file data returned by the webdav lib - * @param {String} prefixPath path to substract from the files - * @returns {Object} - */ -export default function(fileData, prefixPath = '') { - const filename = fileData.filename.replace(prefixPath, '/').replace(/^\/\//, '/') - return Object.assign({ - id: parseInt(fileData.props.fileid), - isFavorite: fileData.props.favorite !== '0', - hasPreview: fileData.props['has-preview'] !== 'false', - }, fileData, { filename }) +const isNumber = function(num) { + if (!num) { + return false + } + return Number(num).toString() === num.toString() } + +export { isNumber } diff --git a/src/views/Albums.vue b/src/views/Albums.vue index 98642e3d..2338a3c7 100644 --- a/src/views/Albums.vue +++ b/src/views/Albums.vue @@ -35,7 +35,7 @@ - + diff --git a/src/views/Tags.vue b/src/views/Tags.vue index 8180b2bc..069b99f4 100644 --- a/src/views/Tags.vue +++ b/src/views/Tags.vue @@ -33,12 +33,23 @@ --> + + + + - Test