diff options
-rw-r--r-- | appinfo/app.php | 24 | ||||
-rw-r--r-- | css/icons.scss | 27 | ||||
-rw-r--r-- | img/app.svg | 2 | ||||
-rw-r--r-- | img/photos.svg | 1 | ||||
-rw-r--r-- | lib/AppInfo/Application.php | 4 | ||||
-rw-r--r-- | lib/Controller/PageController.php | 5 | ||||
-rw-r--r-- | package-lock.json | 26 | ||||
-rw-r--r-- | package.json | 2 | ||||
-rw-r--r-- | src/Photos.vue (renamed from src/Gallery.vue) | 20 | ||||
-rw-r--r-- | src/components/Folder.vue | 27 | ||||
-rw-r--r-- | src/components/Grid.vue | 87 | ||||
-rw-r--r-- | src/components/Navigation.vue | 16 | ||||
-rw-r--r-- | src/main.js | 6 | ||||
-rw-r--r-- | src/router/index.js | 68 | ||||
-rw-r--r-- | src/services/DavClient.js | 3 | ||||
-rw-r--r-- | src/services/FileList.js | 24 | ||||
-rw-r--r-- | src/services/FolderInfo.js | 14 | ||||
-rw-r--r-- | src/services/PhotoSearch.js | 6 | ||||
-rw-r--r-- | src/services/SystemTags.js | 56 | ||||
-rw-r--r-- | src/store/index.js | 2 | ||||
-rw-r--r-- | src/store/systemtags.js | 99 | ||||
-rw-r--r-- | src/utils/ParseFile.js | 37 | ||||
-rw-r--r-- | src/views/Albums.vue (renamed from src/views/Grid.vue) | 60 | ||||
-rw-r--r-- | src/views/Tags.vue | 111 | ||||
-rw-r--r-- | templates/main.php | 23 | ||||
-rw-r--r-- | webpack.common.js | 36 |
26 files changed, 659 insertions, 127 deletions
diff --git a/appinfo/app.php b/appinfo/app.php new file mode 100644 index 00000000..12482fcd --- /dev/null +++ b/appinfo/app.php @@ -0,0 +1,24 @@ +<?php +/** + * @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/>. + * + */ +use OCA\Photos\AppInfo\Application; +\OC::$server->query(Application::class); diff --git a/css/icons.scss b/css/icons.scss new file mode 100644 index 00000000..88e42707 --- /dev/null +++ b/css/icons.scss @@ -0,0 +1,27 @@ +/** + * @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/>. + * + */ + +.icon-folder.icon-dark { + @include icon-color('folder', 'filetypes', $color-black, 1, true); +} + +@include icon-black-white('photos', 'photos', 1); diff --git a/img/app.svg b/img/app.svg index 0779a5dd..04aaca9e 100644 --- a/img/app.svg +++ b/img/app.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="32" width="32" version="1.0"><path fill="#fff" d="M2.69 4a.9.9 0 00-.69.88v22.25c0 .46.42.87.88.87h26.25c.45 0 .87-.42.87-.87V5.22C30 4.55 29.47 4 28.97 4zM4 6h24v10l-2-2-6 8-6-6-8 8H4zm5 2a3 3 0 100 6 3 3 0 000-6z"/></svg>
\ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" height="32" width="32" version="1"><path d="M2.8 4a1.3 1.3 0 00-1.3 1.3v22.4c0 .6.7 1.3 1.3 1.3h26.4c.6 0 1.3-.7 1.3-1.3V5.3c0-.6-.7-1.3-1.3-1.3zm.7 2h25v19h-25z" fill="#fff"/><circle cx="8.5" cy="11.2" r="3" fill="#fff"/><path d="M26.4 14.5l-4.7 6.2L20 23l-1.6-1.8-4.5-4.6-6 5.7-4.7 4.3h26.2v-8.7z" fill="#fff"/></svg>
\ No newline at end of file diff --git a/img/photos.svg b/img/photos.svg new file mode 100644 index 00000000..90c3fdb6 --- /dev/null +++ b/img/photos.svg @@ -0,0 +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>
\ No newline at end of file diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 52139b4d..97ab98b4 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -28,9 +28,9 @@ use OCP\AppFramework\App; class Application extends App { - const appID = 'photos'; + const APP_ID = 'photos'; public function __construct() { - parent::__construct(self::appID); + parent::__construct(self::APP_ID); } } diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php index 7f9538a6..8529441e 100644 --- a/lib/Controller/PageController.php +++ b/lib/Controller/PageController.php @@ -30,6 +30,7 @@ use OCP\AppFramework\Controller; use OCP\AppFramework\Http\TemplateResponse; use OCP\EventDispatcher\IEventDispatcher; use OCP\IRequest; +use OCP\Util; class PageController extends Controller { @@ -58,6 +59,10 @@ class PageController extends Controller { $this->eventDispatcher->dispatch(LoadSidebar::class, new LoadSidebar()); $this->eventDispatcher->dispatch(LoadViewer::class, new LoadViewer()); + + Util::addScript('photos', 'photos'); + Util::addStyle('photos', 'icons'); + $response = new TemplateResponse($this->appName, 'main'); return $response; } diff --git a/package-lock.json b/package-lock.json index c53e7e30..39a3b402 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "gallery", + "name": "photos", "version": "19.0.0", "lockfileVersion": 1, "requires": true, @@ -872,18 +872,18 @@ } }, "@nextcloud/vue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@nextcloud/vue/-/vue-1.0.0.tgz", - "integrity": "sha512-jYggwGf9so7g9uWP59cLspSo62uN7qX0+T096T/QBxyiEhoa+yspf9+Py/RIDylLacceKPDLTU4AdxELj63ZYQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@nextcloud/vue/-/vue-1.1.0.tgz", + "integrity": "sha512-SGWrNTalT/59vElPnPJZ0xQb9sui1yq5fKRiIA2w81NlAK9JQGq+ADeqdVb8sciVnLGobQJ1qERhpo3pIyOhaQ==", "requires": { "@babel/polyfill": "^7.4.4", "@nextcloud/axios": "^0.4.0", "escape-html": "^1.0.3", "hammerjs": "^2.0.8", "md5": "^2.2.1", + "v-click-outside": "^2.1.4", "v-tooltip": "^2.0.0-rc.33", "vue": "^2.6.7", - "vue-click-outside": "^1.0.7", "vue-color": "^2.7.0", "vue-multiselect": "^2.1.3", "vue-visible": "^1.0.2", @@ -9307,6 +9307,11 @@ "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==", "dev": true }, + "v-click-outside": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/v-click-outside/-/v-click-outside-2.1.5.tgz", + "integrity": "sha512-VPNCOTZK6WZy73lcWc+R7IW1uaBFEO3/Csrs5CzWVOdvE30V8Y1+BE/BtTlcEmeDGx0eqdE7bSCg55Jj37PMJg==" + }, "v-tooltip": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/v-tooltip/-/v-tooltip-2.0.2.tgz", @@ -9382,11 +9387,6 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-2.6.10.tgz", "integrity": "sha512-ImThpeNU9HbdZL3utgMCq0oiMzAkt1mcgy3/E6zWC/G6AaQoeuFdsl9nDhTDU3X1R6FK7nsIUuRACVcjI+A2GQ==" }, - "vue-click-outside": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/vue-click-outside/-/vue-click-outside-1.0.7.tgz", - "integrity": "sha1-zdKxYF48SUR4TheU6uShKg9wC9Y=" - }, "vue-color": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/vue-color/-/vue-color-2.7.0.tgz", @@ -9506,9 +9506,9 @@ "integrity": "sha512-yaX2its9XAJKGuQqf7LsiZHHSkxsIK8rmCOQOvEGEoF41blKRK8qr9my4qYoD6ikdLss4n8tKqYBecmaY0+WJg==" }, "vue2-datepicker": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/vue2-datepicker/-/vue2-datepicker-2.13.2.tgz", - "integrity": "sha512-bgtCdSTpFJogL37A5n2HnNPkyKVi0WTiM2+H+fYTHVYbRpSyNaPQ1Kj86A6tx3T14cv6qq4Oo8MrCxXiarDx2w==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/vue2-datepicker/-/vue2-datepicker-2.13.3.tgz", + "integrity": "sha512-kAiTpCLlDC88mMTW5OqhlME0ZSB1fJKlHbKSEryPIi3lRJWHn4BlRSvGUTnSmBUr/5Qidma7Pxei9vih9Luicw==", "requires": { "fecha": "^2.3.3" } diff --git a/package.json b/package.json index 825f4348..abc6819f 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "@nextcloud/axios": "^0.5.0", "@nextcloud/l10n": "^0.2.1", "@nextcloud/router": "^0.1.0", - "@nextcloud/vue": "^1.0.0", + "@nextcloud/vue": "^1.1.0", "cdav-library": "git+https://github.com/nextcloud/cdav-library.git", "path-posix": "^1.0.0", "qs": "^6.9.0", diff --git a/src/Gallery.vue b/src/Photos.vue index cb9a8882..ec26f323 100644 --- a/src/Gallery.vue +++ b/src/Photos.vue @@ -22,6 +22,17 @@ <template> <Content app-name="photos"> + <AppNavigation> + <AppNavigationItem :to="{name: 'root'}" + class="app-navigation__photos" + :title="t('photos', 'Your photos')" + icon="icon-photos" /> + <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: 'maps'}" :title="t('photos', 'Locations')" icon="icon-address" /> + </AppNavigation> <AppContent :class="{ 'icon-loading': loading }"> <router-view v-show="!loading" :loading.sync="loading" /> @@ -34,6 +45,8 @@ <script> import Content from '@nextcloud/vue/dist/Components/Content' import AppContent from '@nextcloud/vue/dist/Components/AppContent' +import AppNavigation from '@nextcloud/vue/dist/Components/AppNavigation' +import AppNavigationItem from '@nextcloud/vue/dist/Components/AppNavigationItem' import svgplaceholder from './assets/img-placeholder.svg' export default { @@ -41,6 +54,8 @@ export default { components: { Content, AppContent, + AppNavigation, + AppNavigationItem, }, data: function() { return { @@ -50,3 +65,8 @@ export default { }, } </script> +<style lang="scss" scoped> +.app-navigation__photos::v-deep .app-navigation-entry-icon.icon-photos { + background-size: 20px; +} +</style> diff --git a/src/components/Folder.vue b/src/components/Folder.vue index 5dec6b5a..de2db5a6 100644 --- a/src/components/Folder.vue +++ b/src/components/Folder.vue @@ -23,7 +23,7 @@ <template> <router-link :class="{'folder--clear': isEmpty}" class="folder" - :to="folder.filename" + :to="to" :aria-label="ariaLabel"> <transition name="fade"> <div v-show="loaded" @@ -39,8 +39,8 @@ </transition> <div class="folder-name"> - <span :class="{'icon-white': !isEmpty}" - class="folder-name__icon icon-folder" + <span :class="[!isEmpty ? 'icon-white' : 'icon-dark', icon]" + class="folder-name__icon" role="img" /> <p :id="ariaUuid" class="folder-name__name"> {{ folder.basename }} @@ -66,6 +66,10 @@ export default { type: Object, required: true, }, + icon: { + type: String, + default: 'icon-folder', + }, }, data() { @@ -106,6 +110,20 @@ export default { ariaLabel() { return t('photos', 'Open the "{name}" sub-directory', { name: this.folder.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 + */ + to() { + const route = Object.assign({}, this.$route, { + // always remove first slash + params: { path: this.folder.filename.substr(1) } + }); + return decodeURIComponent(this.$router.resolve(route).resolved.path) + }, }, async created() { @@ -215,6 +233,9 @@ $name-height: 1.2rem; .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); diff --git a/src/components/Grid.vue b/src/components/Grid.vue new file mode 100644 index 00000000..85023f5a --- /dev/null +++ b/src/components/Grid.vue @@ -0,0 +1,87 @@ +<!-- + - @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> + <!-- Folder content --> + <transition-group + class="photos-grid" + role="grid" + name="list" + tag="div"> + <slot /> + <div key="footer" role="none" class="photos-grid__footer-spacer" /> + </transition-group> +</template> + +<script> +export default { + name: 'Grid', +} +</script> + +<style scoped lang="scss"> +.photos-grid { + display: grid; + align-items: center; + justify-content: center; + gap: 8px; + grid-template-columns: repeat(10, 1fr); + position: relative; + + // always put one more row of grid for the spacer + &__footer-spacer { + // always add one row, so placing it on the first + // column will always add one more + grid-column: 1; + // same height as the width + padding-bottom: 100%; + } +} + +.list-move { + transition: transform var(--animation-quick); +} + +// TODO: use mixins/GridSizes as soon as node-sass supports it +// needs node-sass 5.0 (with libsass 3.6) +// https://github.com/sass/node-sass/pull/2312 +$previous: 0; +@each $size, $config in get('sizes') { + $count: map-get($config, 'count'); + $marginTop: map-get($config, 'marginTop'); + $marginW: map-get($config, 'marginW'); + + // if this is the last entry, only use min-width + $rule: '(min-width: #{$previous}px) and (max-width: #{$size}px)'; + @if $size == 'max' { + $rule: '(min-width: #{$previous}px)'; + } + + @media #{$rule} { + .photos-grid { + padding: #{$marginTop}px #{$marginW}px #{$marginW}px #{$marginW}px; + grid-template-columns: repeat($count, 1fr); + } + } + $previous: $size; +} +</style> diff --git a/src/components/Navigation.vue b/src/components/Navigation.vue index facbdf10..df1c3f0f 100644 --- a/src/components/Navigation.vue +++ b/src/components/Navigation.vue @@ -89,11 +89,25 @@ export default { } return t('photos', 'Back to {folder}', { folder: this.parentName }) }, + + /** + * 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 + */ + to() { + const route = Object.assign({}, this.$route, { + // always remove first slash + params: { path: this.parentPath.substr(1) } + }); + return decodeURIComponent(this.$router.resolve(route).resolved.path) + }, }, methods: { folderUp() { - this.$router.push(this.parentPath) + this.$router.push(this.to) }, }, } diff --git a/src/main.js b/src/main.js index 595ecb95..5e4a96d3 100644 --- a/src/main.js +++ b/src/main.js @@ -26,7 +26,7 @@ import { sync } from 'vuex-router-sync' import { translate, translatePlural } from '@nextcloud/l10n' import Vue from 'vue' -import Gallery from './Gallery' +import Photos from './Photos' import router from './router' import store from './store' @@ -49,8 +49,8 @@ Vue.prototype.n = translatePlural export default new Vue({ el: '#content', // eslint-disable-next-line vue/match-component-file-name - name: 'GalleryRoot', + name: 'PhotosRoot', router, store, - render: h => h(Gallery), + render: h => h(Photos), }) diff --git a/src/router/index.js b/src/router/index.js index 80dcdd5e..df464138 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -24,10 +24,17 @@ import { generateUrl } from '@nextcloud/router' import Router from 'vue-router' import Vue from 'vue' -import Grid from '../views/Grid' +import Albums from '../views/Albums' +import Tags from '../views/Tags' Vue.use(Router) +// shortcut to properly format the path prop +const props = route => ({ + // always lead current path with a slash + path: `/${route.params.path ? route.params.path : ''}`, +}) + export default new Router({ mode: 'history', // if index.php is in the url AND we got this far, then it's working: @@ -37,20 +44,65 @@ export default new Router({ routes: [ { path: '/', - component: Grid, - props: route => ({ - // always lead current path with a slash - path: `/${route.params.path ? route.params.path : ''}`, - }), + component: Albums, name: 'root', + }, + { + path: '/albums', + component: Albums, + name: 'albums', + props, + children: [ + { + path: ':path*', + name: 'path', + component: Albums, + }, + ], + }, + { + path: '/shared', + component: Albums, + name: 'shared', + props, + children: [ + { + path: ':path*', + name: 'path', + component: Albums, + }, + ], + }, + { + path: '/favorites', + component: Tags, + name: 'favorites', + props, children: [ { path: ':path*', name: 'path', - component: Grid, + component: Tags, }, ], }, - { path: '*', redirect: { name: 'root' } }, + { + path: '/tags', + component: Tags, + name: 'tags', + props, + children: [ + { + path: ':path*', + name: 'path', + component: Tags, + }, + ], + }, + { + path: '/maps', + name: 'maps', + redirect: '', + }, ], }) diff --git a/src/services/DavClient.js b/src/services/DavClient.js index acdfdf1b..c91b57e8 100644 --- a/src/services/DavClient.js +++ b/src/services/DavClient.js @@ -24,14 +24,13 @@ import webdav from 'webdav' import axios from '@nextcloud/axios' import parseUrl from 'url-parse' import { generateRemoteUrl } from '@nextcloud/router' -import { getCurrentUser } from '@nextcloud/auth' // force our axios const patcher = webdav.getPatcher() patcher.patch('request', axios) // init webdav client -const remote = generateRemoteUrl(`dav/files/${getCurrentUser().uid}`) +const remote = generateRemoteUrl(`dav`) const client = webdav.createClient(remote) export const remotePath = parseUrl(remote).pathname diff --git a/src/services/FileList.js b/src/services/FileList.js index 2ee84328..80e9df6d 100644 --- a/src/services/FileList.js +++ b/src/services/FileList.js @@ -20,12 +20,14 @@ * */ +import { getCurrentUser } from '@nextcloud/auth' import { getSingleValue, getValueForKey, parseXML, propsToStat } from 'webdav/dist/interface/dav' 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' /** * List files from a folder and filter out unwanted mimes @@ -46,6 +48,8 @@ export default async function(path, options) { details: true, }, options) + const prefixPath = `/files/${getCurrentUser().uid}` + /** * Fetch listing * @@ -54,7 +58,7 @@ export default async function(path, options) { * see https://github.com/perry-mitchell/webdav-client/blob/baf858a4856d44ae19ac12cb10c469b3e6c41ae4/source/interface/directoryContents.js#L11 */ let response = null - const { data } = await client.customRequest(path, options) + const { data } = await client.customRequest(prefixPath + path, options) .then(handleResponseCode) .then(res => { response = res @@ -64,14 +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(entry => { - return Object.assign({ - id: parseInt(entry.props.fileid), - isFavorite: entry.props.favorite !== '0', - hasPreview: entry.props['has-preview'] !== 'false', - }, entry) - }) + const list = data.map(data => parseFile(data, prefixPath)) // filter all the files and folders let folder = {} @@ -91,6 +88,15 @@ export default async function(path, options) { return { folder, folders, files } } +/** + * Modified function to include the root requested folder + * Into the returned data + * + * @param {Object} result the request result + * @param {string} serverBasePath server base path + * @param {boolean} isDetailed detailed request + * @returns {Array} + */ function getDirectoryFiles(result, serverBasePath, isDetailed = false) { const serverBase = pathPosix.join(serverBasePath, '/') // Extract the response items (directory contents) diff --git a/src/services/FolderInfo.js b/src/services/FolderInfo.js index b95d7c67..6d65b57a 100644 --- a/src/services/FolderInfo.js +++ b/src/services/FolderInfo.js @@ -20,8 +20,10 @@ * */ +import { getCurrentUser } from '@nextcloud/auth' import client from './DavClient' import request from './DavRequest' +import parseFile from '../utils/ParseFile' /** * List files from a folder and filter out unwanted mimes @@ -33,17 +35,13 @@ export default async function(path) { // getDirectoryContents doesn't accept / for root const fixedPath = path === '/' ? '' : path + const prefixPath = `/files/${getCurrentUser().uid}` + // fetch listing - const response = await client.stat(fixedPath, { + const response = await client.stat(prefixPath + fixedPath, { data: request, details: true, }) - const entry = response.data - return Object.assign({ - id: parseInt(entry.props.fileid), - isFavorite: entry.props.favorite !== '0', - hasPreview: entry.props['has-preview'] !== 'false', - }, entry) - + return parseFile(response.data, prefixPath) } diff --git a/src/services/PhotoSearch.js b/src/services/PhotoSearch.js index b780ba2c..e4546bb4 100644 --- a/src/services/PhotoSearch.js +++ b/src/services/PhotoSearch.js @@ -20,8 +20,10 @@ * */ -import client from './DavClient' +import { generateRemoteUrl } from '@nextcloud/router' import { getCurrentUser } from '@nextcloud/auth' +import client from './DavClient' +import parseFile from '../utils/ParseFile' /** * List files from a folder and filter out unwanted mimes @@ -35,7 +37,7 @@ export default async function() { headers: { 'content-Type': 'text/xml', }, - url: '/remote.php/dav/', + url: generateRemoteUrl(`dav`), data: `<?xml version="1.0" encoding="UTF-8"?> <d:searchrequest xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" diff --git a/src/services/SystemTags.js b/src/services/SystemTags.js new file mode 100644 index 00000000..a8ce55eb --- /dev/null +++ b/src/services/SystemTags.js @@ -0,0 +1,56 @@ +/** + * @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 client from './DavClient' +import { generateRemoteUrl } from '@nextcloud/router' + +/** + * List files from a folder and filter out unwanted mimes + * + * @returns {Array} the file list + */ +export default async function() { + const response = await client.getDirectoryContents('/systemtags/', { + data: `<?xml version="1.0"?> + <d:propfind xmlns:d="DAV:" + xmlns:oc="http://owncloud.org/ns"> + <d:prop> + <oc:id /> + <oc:display-name /> + <oc:user-visible /> + <oc:user-assignable /> + <oc:can-assign /> + </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) + +} diff --git a/src/store/index.js b/src/store/index.js index d7593570..e48270b7 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -25,12 +25,14 @@ import Vuex, { Store } from 'vuex' import files from './files' import folders from './folders' +import systemtags from './systemtags' Vue.use(Vuex) export default new Store({ modules: { files, folders, + systemtags, }, strict: process.env.NODE_ENV !== 'production', diff --git a/src/store/systemtags.js b/src/store/systemtags.js new file mode 100644 index 00000000..8f213e9e --- /dev/null +++ b/src/store/systemtags.js @@ -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/>. + * + */ +import Vue from 'vue' + +const state = { + paths: {}, + tags: {}, +} + +const mutations = { + /** + * Index folders paths and ids + * + * @param {Object} state vuex state + * @param {Object} data destructuring object + * @param {number} data.id current folder id + * @param {Array} data.files list of files + */ + 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() + }) + + // Set folder list + Vue.set(state.tags, id, list.map(file => file.id)) + } + }, + + /** + * Index folders paths and ids + * + * @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 + */ + addPath(state, { path, id }) { + Vue.set(state.paths, path, id) + }, +} + +const getters = { + tags: state => state.tags, + tag: state => id => state.tags[id], + tagId: state => path => state.paths[path], +} + +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 + */ + 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 })) + }, + + /** + * Index folders paths and ids + * + * @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 + */ + addPath(context, { path, id }) { + context.commit('addPath', { path, id }) + }, +} + +export default { state, mutations, getters, actions } diff --git a/src/utils/ParseFile.js b/src/utils/ParseFile.js new file mode 100644 index 00000000..8d9d9baf --- /dev/null +++ b/src/utils/ParseFile.js @@ -0,0 +1,37 @@ +/** + * @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/>. + * + */ + +/** + * 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 }) +} diff --git a/src/views/Grid.vue b/src/views/Albums.vue index 849fd99b..98642e3d 100644 --- a/src/views/Grid.vue +++ b/src/views/Albums.vue @@ -33,16 +33,11 @@ </EmptyContent> <!-- Folder content --> - <transition-group v-else - class="photos-grid" - role="grid" - name="list" - tag="div"> + <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" /> - <div key="footer" role="none" class="photos-grid__footer-spacer" /> - </transition-group> + </Grid> </template> <script> @@ -55,16 +50,18 @@ import getPictures from '../services/FileList' import EmptyContent from './EmptyContent' import Folder from '../components/Folder' import File from '../components/File' +import Grid from '../components/Grid' import Navigation from '../components/Navigation' import cancelableRequest from '../utils/CancelableRequest' export default { - name: 'Grid', + name: 'Albums', components: { EmptyContent, File, Folder, + Grid, Navigation, }, props: { @@ -205,50 +202,3 @@ export default { } </script> - -<style lang="scss"> -.photos-grid { - display: grid; - align-items: center; - justify-content: center; - gap: 8px; - grid-template-columns: repeat(10, 1fr); - position: relative; - // always put one more row of grid for the spacer - &__footer-spacer { - // always add one row, so placing it on the first - // column will always add one more - grid-column: 1; - // same height as the width - padding-bottom: 100%; - } -} - -.list-move { - transition: transform var(--animation-quick); -} - -// TODO: use mixins/GridSizes as soon as node-sass supports it -// needs node-sass 5.0 (with libsass 3.6) -// https://github.com/sass/node-sass/pull/2312 -$previous: 0; -@each $size, $config in get('sizes') { - $count: map-get($config, 'count'); - $marginTop: map-get($config, 'marginTop'); - $marginW: map-get($config, 'marginW'); - - // if this is the last entry, only use min-width - $rule: '(min-width: #{$previous}px) and (max-width: #{$size}px)'; - @if $size == 'max' { - $rule: '(min-width: #{$previous}px)'; - } - - @media #{$rule} { - .photos-grid { - padding: #{$marginTop}px #{$marginW}px #{$marginW}px #{$marginW}px; - grid-template-columns: repeat($count, 1fr); - } - } - $previous: $size; -} -</style> diff --git a/src/views/Tags.vue b/src/views/Tags.vue new file mode 100644 index 00000000..8180b2bc --- /dev/null +++ b/src/views/Tags.vue @@ -0,0 +1,111 @@ +<!-- + - @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> + <!-- Errors handlers--> + <!-- <EmptyContent v-if="error === 404" illustration-name="folder"> + {{ t('photos', 'This folder does not exists') }} + </EmptyContent> + <EmptyContent v-else-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> --> + + <!-- 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" /> + <File v-for="file in fileList" :key="file.id" v-bind="file" /> + </Grid> --> + <span>Test</span> +</template> + +<script> +import { mapGetters } from 'vuex' + +import getSystemTags from '../services/SystemTags' + +import EmptyContent from './EmptyContent' +import Folder from '../components/Folder' +import File from '../components/File' +import Grid from '../components/Grid' +import Navigation from '../components/Navigation' + +import cancelableRequest from '../utils/CancelableRequest' + +export default { + name: 'Tags', + components: { + EmptyContent, + File, + Folder, + Grid, + Navigation, + }, + props: { + path: { + type: String, + default: '/', + }, + loading: { + type: Boolean, + required: true, + }, + }, + + data() { + return { + error: null, + cancelRequest: () => {}, + } + }, + + computed: { + // global lists + ...mapGetters([ + 'files', + 'tags', + ]), + }, + + watch: { + path(path) { + console.debug('changed:', path) + this.fetchFolderContent() + }, + }, + + async beforeMount() { + console.debug('beforemount: GRID') + this.fetchFolderContent() + }, + + methods: { + async fetchFolderContent() { + await getSystemTags() + }, + }, + +} +</script> diff --git a/templates/main.php b/templates/main.php index 94e41675..1adb9af1 100644 --- a/templates/main.php +++ b/templates/main.php @@ -1,4 +1,25 @@ <?php -script('photos', 'photos'); +/** + * @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/>. + * + */ ?> + <div id="content"></div> diff --git a/webpack.common.js b/webpack.common.js index b9bbc35a..47979290 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -14,13 +14,13 @@ module.exports = { path: path.resolve(__dirname, './js'), publicPath: '/js/', filename: `${appName}.js`, - chunkFilename: 'chunks/[name]-[hash].js' + chunkFilename: 'chunks/[name]-[hash].js', }, module: { rules: [ { test: /\.css$/, - use: ['vue-style-loader', 'css-loader', 'postcss-loader'] + use: ['vue-style-loader', 'css-loader', 'postcss-loader'], }, { test: /\.scss$/, @@ -32,34 +32,34 @@ module.exports = { loader: 'sass-loader', options: { functions: { - 'get($keys)': SassGetGridConfig - } - } - } - ] + 'get($keys)': SassGetGridConfig, + }, + }, + }, + ], }, { test: /\.(js|vue)$/, use: 'eslint-loader', exclude: /node_modules/, - enforce: 'pre' + enforce: 'pre', }, { test: /\.vue$/, loader: 'vue-loader', - exclude: /node_modules/ + exclude: /node_modules/, }, { test: /\.js$/, loader: 'babel-loader', - exclude: /node_modules(?!(\/|\\)(hot-patcher|webdav)(\/|\\))/ + exclude: /node_modules(?!(\/|\\)(hot-patcher|webdav)(\/|\\))/, }, { test: /\.svg$/, // illustrations - loader: 'svg-inline-loader' - } - ] + loader: 'svg-inline-loader', + }, + ], }, plugins: [ new VueLoaderPlugin(), @@ -68,12 +68,12 @@ module.exports = { new ModuleReplaceWebpackPlugin({ modules: [{ test: /request.js/, - replace: './src/patchedRequest.js' - }] - }) + replace: './src/patchedRequest.js', + }], + }), ], resolve: { extensions: ['*', '.js', '.vue'], - symlinks: false - } + symlinks: false, + }, } |