Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
This commit is contained in:
John Molakvoæ (skjnldsv) 2019-11-12 07:12:36 +01:00
parent ae28cc9b2d
commit 2366aaf6ad
No known key found for this signature in database
GPG Key ID: 60C25B8C072916CF
20 changed files with 288 additions and 116 deletions

View File

@ -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);

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="32" width="32" version="1"><g transform="translate(-11.5 2.5)"><path d="M20.5 7.5a1 1 0 00-1 1v17c0 .5.6 1 1 1h20c.5 0 1-.5 1-1v-17c0-.4-.5-1-1-1zM21 9h19v14.5H21z"/><circle cx="24.8" cy="13" r="2.3"/><path d="M38.4 15.5L35 20.2 33.6 22l-1.2-1.4-3.5-3.5-4.5 4.3-3.6 3.3h19.9v-6.6zM14.5 2.5a1 1 0 00-1 1v17c0 .5.6 1 1 1h6v-3H15V4h19v3.5h1.5v-4c0-.4-.5-1-1-1h-20z"/></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" height="32" width="32" version="1"><g transform="translate(-11.5 2.5)"><path d="M20.5 7.5a1 1 0 00-1 1v17c0 .5.6 1 1 1h20c.5 0 1-.5 1-1v-17c0-.4-.5-1-1-1zM21 9h19v14.5H21z"/><circle cx="24.8" cy="13" r="2.3"/><path d="M38.4 15.5L35 20.2 33.6 22l-1.2-1.4-3.5-3.5-4.5 4.3-3.6 3.3h19.9v-6.6l-2.3-2.6zM14.5.5a1 1 0 00-1 1v17c0 .5.6 1 1 1h5.9v-3H15V2h19v6.6h1.5V1.5c0-.4-.5-1-1-1h-20z"/></g></svg>

Before

Width:  |  Height:  |  Size: 422 B

After

Width:  |  Height:  |  Size: 432 B

3
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -30,7 +30,7 @@
<AppNavigationItem to="/favorites" :title="t('photos', 'Favorites')" icon="icon-favorite" />
<AppNavigationItem :to="{name: 'albums'}" :title="t('photos', 'Your albums')" icon="icon-files-dark" />
<AppNavigationItem :to="{name: 'shared'}" :title="t('photos', 'Shared albums')" icon="icon-share" />
<AppNavigationItem :to="{name: 'tags'}" :title="t('photos', 'Tags')" icon="icon-tag" />
<AppNavigationItem :to="{name: 'tags'}" :title="t('photos', 'Tagged photos')" icon="icon-tag" />
<AppNavigationItem :to="{name: 'maps'}" :title="t('photos', 'Locations')" icon="icon-address" />
</AppNavigation>
<AppContent :class="{ 'icon-loading': loading }">

View File

@ -43,7 +43,7 @@
class="folder-name__icon"
role="img" />
<p :id="ariaUuid" class="folder-name__name">
{{ folder.basename }}
{{ basename }}
</p>
</div>
<div class="cover" role="none" />
@ -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) {

View File

@ -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)
},
},

View File

@ -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)

View File

@ -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,
},
],

View File

@ -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 = {}

View File

@ -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)
}

View File

@ -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

View File

@ -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: `<?xml version="1.0"?>
<d:propfind xmlns:d="DAV:"
xmlns:oc="http://owncloud.org/ns">
@ -42,15 +44,7 @@ export default async function() {
</d:prop>
</d:propfind>`,
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))
}

View File

@ -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))

View File

@ -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 })
},
}

125
src/utils/fileUtils.js Normal file
View File

@ -0,0 +1,125 @@
/**
* @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 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 }

View File

@ -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 }

View File

@ -35,7 +35,7 @@
<!-- Folder content -->
<Grid v-else>
<Navigation v-if="folder" key="navigation" v-bind="folder" />
<Folder v-for="dir in folderList" :key="dir.id" :folder="dir" />
<Folder v-for="dir in folderList" :key="dir.id" v-bind="dir" />
<File v-for="file in fileList" :key="file.id" v-bind="file" />
</Grid>
</template>

View File

@ -33,12 +33,23 @@
</EmptyContent> -->
<!-- Folder content -->
<Grid v-if="isRoot">
<Navigation v-if="tag"
key="navigation"
:basename="tagname"
:filename="'/' + tagname"
:root-title="t('photos', 'Tags')" />
<Folder v-for="id in tagsNames"
:key="id"
v-bind="tags[id]"
:basename="tags[id].displayName"
icon="icon-tag" />
</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> -->
<span>Test</span>
</template>
<script>
@ -64,9 +75,9 @@ export default {
Navigation,
},
props: {
path: {
tagname: {
type: String,
default: '/',
default: '',
},
loading: {
type: Boolean,
@ -86,24 +97,66 @@ export default {
...mapGetters([
'files',
'tags',
'tagsNames',
]),
// current tag id from current path
tagId() {
return this.$store.getters.tagId(this.tagname)
},
// current tag
tag() {
return this.tags[this.tagId]
},
// files list of the current tag
fileList() {
return this.tag && this.tag.files
.map(id => this.files[id])
.filter(file => !!file)
},
isRoot() {
return this.tagname === ''
},
},
watch: {
path(path) {
console.debug('changed:', path)
this.fetchFolderContent()
tagname(name) {
console.debug('changed:', name)
this.fetchRootContent()
},
},
async beforeMount() {
console.debug('beforemount: GRID')
this.fetchFolderContent()
this.fetchRootContent()
},
methods: {
async fetchFolderContent() {
await getSystemTags()
async fetchRootContent() {
console.debug('start: fetchRootContent', this.path)
// cancel any pending requests
this.cancelRequest()
// close any potential opened viewer
OCA.Viewer.close()
// if we don't already have some cached data let's show a loader
if (!this.tags[this.folderId]) {
this.$emit('update:loading', true)
}
this.error = null
// init cancellable request
const { request, cancel } = cancelableRequest(getSystemTags)
this.cancelRequest = cancel
const tags = await request()
this.$store.dispatch('updateTags', tags)
// done loading
this.$emit('update:loading', false)
},
},

View File

@ -69,6 +69,7 @@ module.exports = {
modules: [{
test: /request.js/,
replace: './src/patchedRequest.js',
exclude: [/patchedRequest.js$/],
}],
}),
],