Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/photos.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRoeland Jago Douma <roeland@famdouma.nl>2019-11-13 11:19:09 +0300
committerRoeland Jago Douma <roeland@famdouma.nl>2019-11-13 11:19:09 +0300
commitc88707760e0f099df2f43f40ac9dc57c2267c302 (patch)
treed20996fb4049728629bc1ce17b3c8095303c70f8
parent75c185233fe8b1963ed7e20a5539396766624816 (diff)
parent2cf2f147be079dca2a1e7bae9415a7c7a6fd3846 (diff)
Merge remote-tracking branch 'origin/master' into enh/albums
-rw-r--r--appinfo/app.php24
-rw-r--r--css/icons.scss27
-rw-r--r--img/app.svg2
-rw-r--r--img/photos.svg1
-rw-r--r--lib/AppInfo/Application.php4
-rw-r--r--lib/Controller/PageController.php5
-rw-r--r--package-lock.json29
-rw-r--r--package.json3
-rw-r--r--src/Photos.vue (renamed from src/Gallery.vue)20
-rw-r--r--src/components/Folder.vue52
-rw-r--r--src/components/Grid.vue87
-rw-r--r--src/components/Navigation.vue23
-rw-r--r--src/main.js6
-rw-r--r--src/patchedRequest.js3
-rw-r--r--src/router/index.js64
-rw-r--r--src/services/DavClient.js3
-rw-r--r--src/services/FileList.js28
-rw-r--r--src/services/FolderInfo.js14
-rw-r--r--src/services/PhotoSearch.js6
-rw-r--r--src/services/SystemTags.js50
-rw-r--r--src/store/folders.js5
-rw-r--r--src/store/index.js2
-rw-r--r--src/store/systemtags.js99
-rw-r--r--src/utils/fileUtils.js125
-rw-r--r--src/utils/numberUtil.js30
-rw-r--r--src/views/Albums.vue (renamed from src/views/Grid.vue)62
-rw-r--r--src/views/Tags.vue164
-rw-r--r--templates/main.php23
-rw-r--r--webpack.common.js37
29 files changed, 851 insertions, 147 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..ba8dcef9
--- /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', 2);
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..6046f92b
--- /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.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> \ 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..7a8031b8 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",
@@ -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",
@@ -9307,6 +9306,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 +9386,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 +9505,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..4ee8bf4a 100644
--- a/package.json
+++ b/package.json
@@ -35,7 +35,8 @@
"@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",
+ "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/Gallery.vue b/src/Photos.vue
index cb9a8882..49b83e13 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', 'Tagged photos')" 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..c04e17a9 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,11 +39,11 @@
</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 }}
+ {{ basename }}
</p>
</div>
<div class="cover" role="none" />
@@ -62,10 +62,22 @@ export default {
inheritAttrs: false,
props: {
- folder: {
- type: Object,
+ basename: {
+ type: String,
required: true,
},
+ filename: {
+ type: String,
+ required: true,
+ },
+ id: {
+ type: Number,
+ required: true,
+ },
+ icon: {
+ type: String,
+ default: 'icon-folder',
+ },
},
data() {
@@ -84,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
@@ -101,10 +113,25 @@ 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
+ * 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)
},
},
@@ -115,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) {
@@ -215,6 +242,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..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
},
@@ -89,11 +93,26 @@ 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
+ * @returns {string}
+ */
+ 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/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 80dcdd5e..d4d9caa6 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,59 @@ 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: 'albumspath',
+ component: Albums,
+ },
+ ],
+ },
+ {
+ path: '/shared',
+ component: Albums,
+ name: 'shared',
+ props,
children: [
{
path: ':path*',
- name: 'path',
- component: Grid,
+ name: 'sharedpath',
+ component: Albums,
},
],
},
- { path: '*', redirect: { name: 'root' } },
+ {
+ path: '/favorites',
+ component: Tags,
+ name: 'favorites',
+ },
+ {
+ path: '/tags',
+ component: Tags,
+ name: 'tags',
+ props: route => ({
+ tagname: route.params.tagname,
+ }),
+ children: [
+ {
+ path: ':tagname',
+ name: 'tagname',
+ 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..c473aad8 100644
--- a/src/services/FileList.js
+++ b/src/services/FileList.js
@@ -20,12 +20,13 @@
*
*/
+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 { genFileInfo } from '../utils/fileUtils'
/**
* List files from a folder and filter out unwanted mimes
@@ -35,6 +36,8 @@ import request from './DavRequest'
* @returns {Array} the file list
*/
export default async function(path, options) {
+
+ console.trace();
options = Object.assign({
method: 'PROPFIND',
headers: {
@@ -42,10 +45,11 @@ export default async function(path, options) {
Depth: options.deep ? 'infinity' : 1,
},
responseType: 'text',
- data: request,
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 => genFileInfo(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..b58ddb79 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 { genFileInfo } from '../utils/fileUtils'
/**
* 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 genFileInfo(response.data, prefixPath)
}
diff --git a/src/services/PhotoSearch.js b/src/services/PhotoSearch.js
index b780ba2c..8d3789ea 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 { genFileInfo } from '../utils/fileUtils'
/**
* 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..c0244a31
--- /dev/null
+++ b/src/services/SystemTags.js
@@ -0,0 +1,50 @@
+/**
+ * @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 { genFileInfo } from '../utils/fileUtils'
+
+/**
+ * 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(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">
+ <d:prop>
+ <oc:id />
+ <oc:display-name />
+ <oc:user-visible />
+ <oc:user-assignable />
+ <oc:can-assign />
+ </d:prop>
+ </d:propfind>`,
+ details: true,
+ }, 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/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..4a49e13e
--- /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'
+import { sortCompare } from '../utils/fileUtils'
+
+const state = {
+ tags: {},
+ names: {},
+}
+
+const mutations = {
+ /**
+ * Order and save tags
+ *
+ * @param {Object} state vuex state
+ * @param {Array} tags the tags list
+ */
+ updateTags(state, tags) {
+ if (tags.length > 0) {
+ // sort by basename
+ const list = tags.sort((a, b) => sortCompare(a, b, 'displayName'))
+
+ // 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)
+ })
+ }
+ },
+
+ /**
+ * Update tag files list
+ *
+ * @param {Object} state vuex state
+ * @param {Object} data destructuring object
+ * @param {number} data.id current tag id
+ * @param {Object[]} data.files list of files
+ */
+ 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 => name => state.names[name],
+}
+
+const actions = {
+ /**
+ * Update files and folders
+ *
+ * @param {Object} context vuex context
+ * @param {Array} tags the tag list
+ */
+ updateTags(context, tags) {
+ context.commit('updateTags', tags)
+ },
+
+ /**
+ * Update tag files list
+ *
+ * @param {Object} context vuex context
+ * @param {Object} data destructuring object
+ * @param {number} data.id current tag id
+ * @param {Object[]} data.files list of files
+ */
+ updateTag(context, { id, files }) {
+ context.commit('updateTag', { id, files })
+ },
+}
+
+export default { state, mutations, getters, actions }
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æ <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 }
diff --git a/src/utils/numberUtil.js b/src/utils/numberUtil.js
new file mode 100644
index 00000000..0c3a96e5
--- /dev/null
+++ b/src/utils/numberUtil.js
@@ -0,0 +1,30 @@
+/**
+ * @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/>.
+ *
+ */
+
+const isNumber = function(num) {
+ if (!num) {
+ return false
+ }
+ return Number(num).toString() === num.toString()
+}
+
+export { isNumber }
diff --git a/src/views/Grid.vue b/src/views/Albums.vue
index 849fd99b..2338a3c7 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" />
+ <Folder v-for="dir in folderList" :key="dir.id" v-bind="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..069b99f4
--- /dev/null
+++ b/src/views/Tags.vue
@@ -0,0 +1,164 @@
+<!--
+ - @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-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> -->
+</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: {
+ tagname: {
+ type: String,
+ default: '',
+ },
+ loading: {
+ type: Boolean,
+ required: true,
+ },
+ },
+
+ data() {
+ return {
+ error: null,
+ cancelRequest: () => {},
+ }
+ },
+
+ computed: {
+ // global lists
+ ...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: {
+ tagname(name) {
+ console.debug('changed:', name)
+ this.fetchRootContent()
+ },
+ },
+
+ async beforeMount() {
+ console.debug('beforemount: GRID')
+ this.fetchRootContent()
+ },
+
+ methods: {
+ 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)
+ },
+ },
+
+}
+</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..06c06cb3 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,13 @@ module.exports = {
new ModuleReplaceWebpackPlugin({
modules: [{
test: /request.js/,
- replace: './src/patchedRequest.js'
- }]
- })
+ replace: './src/patchedRequest.js',
+ exclude: [/patchedRequest.js$/],
+ }],
+ }),
],
resolve: {
extensions: ['*', '.js', '.vue'],
- symlinks: false
- }
+ symlinks: false,
+ },
}