summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMarcel Klehr <mklehr@gmx.net>2022-08-29 11:32:32 +0200
committerMarcel Klehr <mklehr@gmx.net>2022-09-01 13:41:33 +0200
commit4ae8f27f0e6546ed8bbaa919e4a48eed51055632 (patch)
tree3b7561a66d454954fdc4c7f27ffb1a518b8fe11c
parent46512b4415e588d956cb04ac6bf58e18b891c863 (diff)
Make tags view pretty even if recognize is not installed
Signed-off-by: Marcel Klehr <mklehr@gmx.net>
-rw-r--r--appinfo/routes.php8
-rw-r--r--src/components/FolderTagPreview.vue14
-rw-r--r--src/components/Tag.vue40
-rw-r--r--src/components/ThingsCategory.vue104
-rw-r--r--src/router/index.js25
-rw-r--r--src/services/Things.js23
-rw-r--r--src/store/systemtags.js30
-rw-r--r--src/views/CategoryContent.vue199
-rw-r--r--src/views/TagContent.vue199
-rw-r--r--src/views/Tags.vue221
10 files changed, 682 insertions, 181 deletions
diff --git a/appinfo/routes.php b/appinfo/routes.php
index 380a0b5e..ba81f4c6 100644
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -78,6 +78,14 @@ return [
'path' => '',
]
],
+ ['name' => 'page#index', 'url' => '/categories/{path}', 'verb' => 'GET', 'postfix' => 'categories',
+ 'requirements' => [
+ 'path' => '.*',
+ ],
+ 'defaults' => [
+ 'path' => '',
+ ]
+ ],
// apis
[
diff --git a/src/components/FolderTagPreview.vue b/src/components/FolderTagPreview.vue
index 77a24bea..ab4e59b7 100644
--- a/src/components/FolderTagPreview.vue
+++ b/src/components/FolderTagPreview.vue
@@ -23,7 +23,7 @@
<template>
<router-link :class="{'folder--clear': isEmpty}"
class="folder"
- :to="to"
+ :to="toLink"
:aria-label="ariaLabel">
<!-- Images preview -->
<transition name="fade">
@@ -74,12 +74,16 @@ export default {
},
path: {
type: String,
- required: true,
+ default: '',
},
fileList: {
type: Array,
default: () => [],
},
+ to: {
+ type: Object,
+ default: null,
+ },
},
data() {
@@ -121,7 +125,10 @@ export default {
*
* @return {string}
*/
- to() {
+ toLink() {
+ if (this.to) {
+ return this.to
+ }
// always remove first slash, the router
// manage it automatically
const regex = /^\/?(.+)/i
@@ -215,6 +222,7 @@ $name-height: 1rem;
// Cover management empty/full
.folder {
+ border-radius: var(--border-radius-large);
// if no img, let's display the folder icon as default black
&--clear {
.folder-name__icon {
diff --git a/src/components/Tag.vue b/src/components/Tag.vue
index 49aaa2fa..a537cc7b 100644
--- a/src/components/Tag.vue
+++ b/src/components/Tag.vue
@@ -22,17 +22,18 @@
-->
<template>
- <FolderTagPreview :id="item.injected.id"
- icon="icon-tag"
- :name="item.injected.displayName"
- :path="item.injected.displayName"
- :file-list="fileList" />
+ <div class="tag">
+ <FolderTagPreview :id="tag.id"
+ icon="icon-tag"
+ :name="t('recognize', tag.displayName)"
+ :to="{name: 'tagcontent', params: {path: tag.displayName }}"
+ :file-list="fileList" />
+ </div>
</template>
<script>
import { mapGetters } from 'vuex'
-import getTaggedImages from '../services/TaggedImages'
import FolderTagPreview from './FolderTagPreview'
import AbortControllerMixin from '../mixins/AbortControllerMixin'
@@ -49,7 +50,7 @@ export default {
inheritAttrs: false,
props: {
- item: {
+ tag: {
type: Object,
required: true,
},
@@ -64,7 +65,7 @@ export default {
// files list of the current folder
folderContent() {
- return this.tags[this.item.injected.id].files
+ return this.tags[this.tag.id].files
},
fileList() {
return this.folderContent
@@ -77,19 +78,16 @@ export default {
},
async created() {
- try {
- // get data
- const files = await getTaggedImages(this.item.injected.id, {
- signal: this.abortController.signal,
- })
- this.$store.dispatch('updateTag', { id: this.item.injected.id, files })
- this.$store.dispatch('appendFiles', files)
- } catch (error) {
- if (error.response && error.response.status) {
- console.error('Failed to get folder content', this.item.injected.id, error.response)
- }
- }
+ this.$store.dispatch('fetchTagFiles', {
+ id: this.tag.id,
+ signal: this.abortController.signal,
+ })
},
-
}
</script>
+<style scoped lang="scss">
+.tag {
+ height: 250px;
+ width: 250px;
+}
+</style>
diff --git a/src/components/ThingsCategory.vue b/src/components/ThingsCategory.vue
new file mode 100644
index 00000000..80d0af3d
--- /dev/null
+++ b/src/components/ThingsCategory.vue
@@ -0,0 +1,104 @@
+<!--
+ - @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
+ -
+ - @author John Molakvoæ <skjnldsv@protonmail.com>
+ - @author Corentin Mors <medias@pixelswap.fr>
+ -
+ - @license AGPL-3.0-or-later
+ -
+ - 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>
+ <div v-show="folderContent.length" class="things-category">
+ <FolderTagPreview :id="Object.keys(CATEGORIES).indexOf(title)"
+ icon="icon-tag"
+ :name="t('photos', title)"
+ :to="{name:'categorycontent', params:{category: title}}"
+ :file-list="fileList" />
+ </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+
+import FolderTagPreview from './FolderTagPreview'
+import { CATEGORIES } from '../services/Things'
+
+export default {
+ name: 'ThingsCategory',
+
+ components: {
+ FolderTagPreview,
+ },
+ inheritAttrs: false,
+
+ props: {
+ title: {
+ type: String,
+ required: true,
+ },
+ },
+
+ data() {
+ return {
+ CATEGORIES,
+ }
+ },
+
+ computed: {
+ // global lists
+ ...mapGetters([
+ 'files',
+ 'tags',
+ 'tagsNames',
+ ]),
+
+ // tags list of the current category
+ categoryTags() {
+ return CATEGORIES[this.title]
+ .map(tagName => this.tags[this.tagsNames[tagName]])
+ .filter(Boolean)
+ },
+
+ // files list of the current category
+ folderContent() {
+ return this.categoryTags.flatMap(tag => tag.files)
+ },
+
+ fileList() {
+ return this.folderContent
+ ? this.folderContent
+ .map(id => this.files[id])
+ .filter(file => !!file)
+ .slice(0, 4) // only get the 4 first images
+ : []
+ },
+ },
+
+ async created() {
+ Promise.all(this.categoryTags.map(tag =>
+ this.$store.dispatch('fetchTagFiles', { id: tag.id })
+ ))
+ },
+
+}
+</script>
+<style scoped lang="scss">
+.things-category {
+ height: 250px;
+ width: 250px;
+}
+</style>
diff --git a/src/router/index.js b/src/router/index.js
index 99056906..6dd62c92 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -34,6 +34,8 @@ const Folders = () => import('../views/Folders')
const Albums = () => import('../views/Albums')
const AlbumContent = () => import('../views/AlbumContent')
const Tags = () => import('../views/Tags')
+const TagContent = () => import('../views/TagContent')
+const CategoryContent = () => import('../views/CategoryContent')
const Timeline = () => import('../views/Timeline')
const Faces = () => import('../views/Faces')
const FaceContent = () => import('../views/FaceContent')
@@ -129,18 +131,35 @@ const router = new Router({
}),
},
{
- path: '/tags/:path*',
+ path: '/tags/',
component: Tags,
name: 'tags',
redirect: !areTagsInstalled ? { name: 'timeline' } : null,
props: route => ({
- path: `${route.params.path ? route.params.path : ''}`,
- // if path is empty
+ path: '',
isRoot: !route.params.path,
rootTitle: t('photos', 'Tagged photos'),
}),
},
{
+ path: '/tags/:path',
+ component: TagContent,
+ name: 'tagcontent',
+ redirect: !areTagsInstalled ? { name: 'timeline' } : null,
+ props: route => ({
+ path: `${route.params.path ? route.params.path : ''}`,
+ }),
+ },
+ {
+ path: '/categories/:category',
+ component: CategoryContent,
+ name: 'categorycontent',
+ redirect: !areTagsInstalled ? { name: 'timeline' } : null,
+ props: route => ({
+ category: route.params.category,
+ }),
+ },
+ {
path: '/maps',
name: 'maps',
// router-link doesn't support external url, let's force the redirect
diff --git a/src/services/Things.js b/src/services/Things.js
new file mode 100644
index 00000000..544772a7
--- /dev/null
+++ b/src/services/Things.js
@@ -0,0 +1,23 @@
+export const CATEGORIES = {
+ Nature: ['Outdoor', 'Nature', 'Plant', 'Wildlife', 'Landscape'],
+ Architecture: ['Architecture', 'Historic', 'Monument'],
+ Food: ['Food', 'Beverage', 'Cooking', 'Vegetables', 'Fruit', 'Dining', 'Plate'],
+ Music: ['Music'],
+ Water: ['Water', 'Seaside', 'Lakeside'],
+ Mountains: ['Alpine', 'Mountain'],
+ Snow: ['Snow'],
+ Beach: ['Beach'],
+ Animals: ['Animal'],
+ People: ['People', 'Portrait'],
+}
+const t = () => {}
+t('photos', 'Nature')
+t('photos', 'Architecture')
+t('photos', 'Food')
+t('photos', 'Music')
+t('photos', 'Water')
+t('photos', 'Mountains')
+t('photos', 'Snow')
+t('photos', 'Beach')
+t('photos', 'Animals')
+t('photos', 'People')
diff --git a/src/store/systemtags.js b/src/store/systemtags.js
index bcfc8426..c8bb30ea 100644
--- a/src/store/systemtags.js
+++ b/src/store/systemtags.js
@@ -21,6 +21,9 @@
*/
import Vue from 'vue'
import { sortCompare } from '../utils/fileUtils'
+import getTaggedImages from '../services/TaggedImages'
+import { abortController } from '../services/RequestHandler'
+import getSystemTags from '../services/SystemTags'
const state = {
tags: {},
@@ -118,6 +121,33 @@ const actions = {
}
context.commit('updateTag', { id, files })
},
+
+ /**
+ *
+ * @param context
+ * @param id.id
+ * @param id the tag id to fetch files for
+ * @return {Promise<void>}
+ */
+ async fetchTagFiles(context, { id }) {
+ try {
+ // get data
+ const files = await getTaggedImages(id, { signal: abortController.signal })
+ await context.dispatch('updateTag', { id, files })
+ await context.dispatch('appendFiles', files)
+ } catch (error) {
+ if (error.response && error.response.status) {
+ console.error('Failed to get tag content', id, error.response)
+ }
+ }
+ },
+
+ async fetchAllTags(context) {
+ const tags = await getSystemTags('', {
+ signal: abortController.signal,
+ })
+ await context.dispatch('updateTags', tags)
+ },
}
export default { state, mutations, getters, actions }
diff --git a/src/views/CategoryContent.vue b/src/views/CategoryContent.vue
new file mode 100644
index 00000000..9ad1e941
--- /dev/null
+++ b/src/views/CategoryContent.vue
@@ -0,0 +1,199 @@
+<!--
+ - @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
+ -
+ - @author John Molakvoæ <skjnldsv@protonmail.com>
+ - @author Corentin Mors <medias@pixelswap.fr>
+ - @author Marcel Klehr <mklehr@gmx.net>
+ -
+ - @license AGPL-3.0-or-later
+ -
+ - 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">
+ {{ t('photos', 'An error occurred') }}
+ </EmptyContent>
+
+ <!-- Folder content -->
+ <div v-else-if="!loading">
+ <div class="photos-navigation">
+ <Actions class="photos-navigation__back">
+ <ActionButton @click="$router.push({name: 'tags'})">
+ <template #icon>
+ <ArrowLeft />
+ </template>
+ {{ t('photos', 'Back to tags overview') }}
+ </ActionButton>
+ </Actions>
+ <h2 class="photos-navigation__title">
+ {{ t('photos', category) }}
+ </h2>
+ </div>
+ <EmptyContent v-if="isEmpty" key="emptycontent" illustration-name="empty">
+ {{ t('photos', 'No tags yet') }}
+ <template #desc>
+ {{ t('photos', 'Photos with tags will show up here') }}
+ </template>
+ </EmptyContent>
+
+ <FilesListViewer class="category__photos"
+ :use-window="true"
+ :file-ids="fileIds"
+ :base-height="isMobile ? 120 : 200"
+ :loading="loading">
+ <File slot-scope="{file, visibility}"
+ :file="files[file.id]"
+ :allow-selection="true"
+ :selected="selection[file.id] === true"
+ :visibility="visibility"
+ :semaphore="semaphore"
+ @click="openViewer"
+ @select-toggled="onFileSelectToggle" />
+ </FilesListViewer>
+ </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+import Actions from '@nextcloud/vue/dist/Components/Actions'
+import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
+import ArrowLeft from 'vue-material-design-icons/ArrowLeft'
+
+import EmptyContent from '../components/EmptyContent'
+import File from '../components/File'
+
+import FilesListViewer from '../components/FilesListViewer'
+import { isMobile } from '@nextcloud/vue'
+import SemaphoreWithPriority from '../utils/semaphoreWithPriority'
+import FilesSelectionMixin from '../mixins/FilesSelectionMixin'
+import { CATEGORIES } from '../services/Things'
+
+export default {
+ name: 'CategoryContent',
+ components: {
+ File,
+ FilesListViewer,
+ EmptyContent,
+ Actions,
+ ActionButton,
+ ArrowLeft,
+ },
+ mixins: [
+ isMobile,
+ FilesSelectionMixin,
+ ],
+ props: {
+ category: {
+ type: String,
+ required: true,
+ },
+ },
+
+ data() {
+ return {
+ error: null,
+ loading: false,
+ semaphore: new SemaphoreWithPriority(30),
+ }
+ },
+
+ computed: {
+ // global lists
+ ...mapGetters([
+ 'files',
+ 'tags',
+ 'tagsNames',
+ ]),
+
+ // current tag id from current path
+ tagIds() {
+ return CATEGORIES[this.category]
+ .map(tagName => this.tagsNames[tagName])
+ .filter(Boolean)
+ },
+
+ // files list of the current category
+ fileIds() {
+ return [...new Set(this.tagIds.flatMap(tagId => this.tags[tagId].files))]
+ },
+
+ isEmpty() {
+ return this.fileIds.length === 0
+ },
+ },
+
+ watch: {
+ async path() {
+ this.fetchContent()
+ },
+ },
+
+ async beforeMount() {
+ this.fetchContent()
+ },
+
+ methods: {
+ async fetchContent() {
+ // close any potential opened viewer
+ OCA.Viewer.close()
+
+ this.loading = true
+
+ if (!this.tagIds.length) {
+ await this.$store.dispatch('fetchAllTags')
+ }
+ this.error = null
+
+ try {
+ await Promise.all(this.tagIds.map(tagId =>
+ this.$store.dispatch('fetchTagFiles', { id: tagId })
+ ))
+ } catch (error) {
+ console.error(error)
+ this.error = true
+ } finally {
+ // done loading
+ this.loading = false
+ }
+ },
+
+ openViewer(fileId) {
+ const file = this.files[fileId]
+ OCA.Viewer.open({
+ path: file.filename,
+ list: this.fileIds.map(fileId => this.files[fileId]),
+ loadMore: file.loadMore ? async () => await file.loadMore(true) : () => [],
+ canLoop: file.canLoop,
+ })
+ },
+ },
+}
+</script>
+<style scoped lang="scss">
+.photos-navigation {
+ display: flex;
+ height: 44px;
+ padding: 0 40px;
+ align-items: center;
+ max-width: 100%;
+
+ h2 {
+ padding: 0;
+ margin: 0;
+ }
+}
+</style>
diff --git a/src/views/TagContent.vue b/src/views/TagContent.vue
new file mode 100644
index 00000000..ccb02f00
--- /dev/null
+++ b/src/views/TagContent.vue
@@ -0,0 +1,199 @@
+<!--
+ - @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
+ -
+ - @author John Molakvoæ <skjnldsv@protonmail.com>
+ - @author Corentin Mors <medias@pixelswap.fr>
+ - @author Marcel Klehr <mklehr@gmx.net>
+ -
+ - @license AGPL-3.0-or-later
+ -
+ - 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">
+ {{ t('photos', 'An error occurred') }}
+ </EmptyContent>
+
+ <!-- Folder content -->
+ <div v-else-if="!loading">
+ <div class="photos-navigation">
+ <Actions class="photos-navigation__back">
+ <ActionButton @click="$router.push({name: 'tags'})">
+ <template #icon>
+ <ArrowLeft />
+ </template>
+ {{ t('photos', 'Back to tags overview') }}
+ </ActionButton>
+ </Actions>
+ <h2 class="photos-navigation__title">
+ {{ path }}
+ </h2>
+ </div>
+ <EmptyContent v-if="isEmpty" key="emptycontent" illustration-name="empty">
+ {{ t('photos', 'No tags yet') }}
+ <template #desc>
+ {{ t('photos', 'Photos with tags will show up here') }}
+ </template>
+ </EmptyContent>
+
+ <FilesListViewer class="tag__photos"
+ :use-window="true"
+ :file-ids="fileIds"
+ :base-height="isMobile ? 120 : 200"
+ :loading="loading">
+ <File slot-scope="{file, visibility}"
+ :file="files[file.id]"
+ :allow-selection="true"
+ :selected="selection[file.id] === true"
+ :visibility="visibility"
+ :semaphore="semaphore"
+ @click="openViewer"
+ @select-toggled="onFileSelectToggle" />
+ </FilesListViewer>
+ </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+
+import EmptyContent from '../components/EmptyContent'
+import File from '../components/File'
+
+import FilesListViewer from '../components/FilesListViewer'
+import { isMobile } from '@nextcloud/vue'
+import SemaphoreWithPriority from '../utils/semaphoreWithPriority'
+import FilesSelectionMixin from '../mixins/FilesSelectionMixin'
+import Actions from '@nextcloud/vue/dist/Components/Actions'
+import ActionButton from '@nextcloud/vue/dist/Components/ActionButton'
+import ArrowLeft from 'vue-material-design-icons/ArrowLeft'
+
+export default {
+ name: 'TagContent',
+ components: {
+ File,
+ FilesListViewer,
+ EmptyContent,
+ Actions,
+ ActionButton,
+ ArrowLeft,
+ },
+ mixins: [
+ isMobile,
+ FilesSelectionMixin,
+ ],
+ props: {
+ path: {
+ type: String,
+ default: '',
+ },
+ },
+
+ data() {
+ return {
+ error: null,
+ loading: false,
+ semaphore: new SemaphoreWithPriority(30),
+ }
+ },
+
+ computed: {
+ // global lists
+ ...mapGetters([
+ 'files',
+ 'tags',
+ 'tagsNames',
+ ]),
+
+ // current tag id from current path
+ tagId() {
+ return this.$store.getters.tagId(this.path)
+ },
+
+ // current tag
+ tag() {
+ return this.tags[this.tagId]
+ },
+
+ // files list of the current tag
+ fileIds() {
+ return this.tag ? this.tag.files : []
+ },
+
+ isEmpty() {
+ return this.fileIds.length === 0
+ },
+ },
+
+ watch: {
+ async path() {
+ this.fetchContent()
+ },
+ },
+
+ async beforeMount() {
+ this.fetchContent()
+ },
+
+ methods: {
+ async fetchContent() {
+ // 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.tagId]) {
+ this.loading = true
+ await this.$store.dispatch('fetchAllTags')
+ }
+ this.error = null
+
+ try {
+ await this.$store.dispatch('fetchTagFiles', { id: this.tagId })
+ } catch (error) {
+ console.error(error)
+ this.error = true
+ } finally {
+ // done loading
+ this.loading = false
+ }
+ },
+
+ openViewer(fileId) {
+ const file = this.files[fileId]
+ OCA.Viewer.open({
+ path: file.filename,
+ list: this.fileIds.map(fileId => this.files[fileId]),
+ loadMore: file.loadMore ? async () => await file.loadMore(true) : () => [],
+ canLoop: file.canLoop,
+ })
+ },
+ },
+}
+</script>
+<style scoped lang="scss">
+.photos-navigation {
+ display: flex;
+ height: 44px;
+ padding: 0 40px;
+ align-items: center;
+ max-width: 100%;
+
+ h2 {
+ padding: 0;
+ margin: 0;
+ }
+}
+</style>
diff --git a/src/views/Tags.vue b/src/views/Tags.vue
index fd200d3b..aec821af 100644
--- a/src/views/Tags.vue
+++ b/src/views/Tags.vue
@@ -22,78 +22,69 @@
-->
<template>
- <!-- Errors handlers-->
- <EmptyContent v-if="error">
- {{ t('photos', 'An error occurred') }}
- </EmptyContent>
+ <div>
+ <!-- Errors handlers-->
+ <EmptyContent v-if="error">
+ {{ t('photos', 'An error occurred') }}
+ </EmptyContent>
- <!-- Folder content -->
- <div v-else-if="!loading">
- <Navigation key="navigation"
- :basename="path"
- :filename="'/' + path"
- :root-title="rootTitle" />
- <EmptyContent v-if="isEmpty" key="emptycontent" illustration-name="empty">
+ <EmptyContent v-if="!loading && !hasTagsWithFiles" key="emptycontent" illustration-name="empty">
{{ t('photos', 'No tags yet') }}
<template #desc>
{{ t('photos', 'Photos with tags will show up here') }}
</template>
</EmptyContent>
- <div v-else class="grid-container">
- <VirtualGrid ref="virtualgrid"
- :items="contentList"
- :get-column-count="() => gridConfig.count"
- :get-grid-gap="() => gridConfig.gap" />
+ <Loader v-if="loading" class="loader" />
+
+ <div v-else>
+ <div class="grid-container">
+ <div v-if="hasCategoriesWithFiles" class="things">
+ <ThingsCategory v-for="category in Object.keys(CATEGORIES)" :key="category" :title="category" />
+ <div v-if="!showTags" class="expand-box">
+ <Button aria-label="Show more tags" @click="expandTags()">
+ More
+ </Button>
+ </div>
+ </div>
+ <div v-if="showTags || !hasCategoriesWithFiles" class="tags">
+ <Tag v-for="tag in tagsList" :key="tag.id" :tag="tag" />
+ </div>
+ </div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
-import VirtualGrid from 'vue-virtual-grid'
-
-import getSystemTags from '../services/SystemTags'
-import getTaggedImages from '../services/TaggedImages'
+import { Button } from '@nextcloud/vue'
import EmptyContent from '../components/EmptyContent'
import Tag from '../components/Tag'
-import File from '../components/File'
-import Navigation from '../components/Navigation'
-import GridConfigMixin from '../mixins/GridConfig'
+import { CATEGORIES } from '../services/Things'
+import ThingsCategory from '../components/ThingsCategory'
+import Loader from '../components/Loader'
import AbortControllerMixin from '../mixins/AbortControllerMixin'
+
export default {
name: 'Tags',
components: {
- VirtualGrid,
+ Loader,
+ ThingsCategory,
+ Tag,
EmptyContent,
- Navigation,
- },
- mixins: [
- GridConfigMixin,
- AbortControllerMixin,
- ],
- props: {
- rootTitle: {
- type: String,
- required: true,
- },
- path: {
- type: String,
- default: '',
- },
- isRoot: {
- type: Boolean,
- default: true,
- },
+ Button,
},
+ mixins: [AbortControllerMixin],
data() {
return {
error: null,
loading: false,
+ showTags: false,
+ CATEGORIES,
}
},
@@ -105,93 +96,24 @@ export default {
'tagsNames',
]),
- // current tag id from current path
- tagId() {
- return this.$store.getters.tagId(this.path)
- },
-
- // current tag
- tag() {
- return this.tags[this.tagId]
- },
-
tagsList() {
- return Object.values(this.tagsNames).map((tagsId) => this.tags[tagsId])
+ return Object.values(this.tagsNames).map((tagsId) => this.tags[tagsId]).filter(tag => tag && tag.id)
},
- // files list of the current tag
- fileList() {<