aboutsummaryrefslogtreecommitdiffstats
path: root/core/src/components/AppMenu.vue
diff options
context:
space:
mode:
authorJulius Härtl <jus@bitgrid.net>2022-08-27 10:57:13 +0200
committerJulius Härtl <jus@bitgrid.net>2022-08-31 10:24:03 +0200
commit5b4708c5be100c3a4bbb2fd32151ae2a7420df2d (patch)
tree181dc1020643ecc5434fa77bfac5606be3826ed4 /core/src/components/AppMenu.vue
parent23bb4f16f9056e7a79116129c7de5b59cf84f8be (diff)
downloadnextcloud-server-5b4708c5be100c3a4bbb2fd32151ae2a7420df2d.tar.gz
nextcloud-server-5b4708c5be100c3a4bbb2fd32151ae2a7420df2d.zip
Move app menu to vue
Signed-off-by: Julius Härtl <jus@bitgrid.net>
Diffstat (limited to 'core/src/components/AppMenu.vue')
-rw-r--r--core/src/components/AppMenu.vue268
1 files changed, 268 insertions, 0 deletions
diff --git a/core/src/components/AppMenu.vue b/core/src/components/AppMenu.vue
new file mode 100644
index 00000000000..a8ea7250852
--- /dev/null
+++ b/core/src/components/AppMenu.vue
@@ -0,0 +1,268 @@
+<!--
+ - @copyright Copyright (c) 2022 Julius Härtl <jus@bitgrid.net>
+ -
+ - @author Julius Härtl <jus@bitgrid.net>
+ -
+ - @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>
+ <nav class="app-menu">
+ <ul class="app-menu-main">
+ <li v-for="app in mainAppList"
+ :key="app.id"
+ :data-app-id="app.id"
+ class="app-menu-entry"
+ :class="{ 'app-menu-entry__active': app.active }">
+ <a :href="app.href" :class="{ 'has-unread': app.unread > 0 }" :aria-label="appLabel(app)">
+ <img :src="app.icon" alt="">
+ <div class="app-menu-entry--label">
+ {{ app.name }}
+ <span v-if="app.unread > 0" class="hidden-visually unread-counter">{{ app.unread }}</span>
+ </div>
+ </a>
+ </li>
+ </ul>
+ <NcActions class="app-menu-more" :aria-label="t('core', 'More apps')">
+ <NcActionLink v-for="app in popoverAppList"
+ :key="app.id"
+ :aria-label="appLabel(app)"
+ :href="app.href"
+ class="app-menu-popover-entry">
+ <template #icon>
+ <div class="app-icon" :class="{ 'has-unread': app.unread > 0 }">
+ <img :src="app.icon" alt="">
+ </div>
+ </template>
+ {{ app.name }}
+ <span v-if="app.unread > 0" class="hidden-visually unread-counter">{{ app.unread }}</span>
+ </NcActionLink>
+ </NcActions>
+ </nav>
+</template>
+
+<script>
+import { loadState } from '@nextcloud/initial-state'
+import NcActions from '@nextcloud/vue/dist/Components/NcActions'
+import NcActionLink from '@nextcloud/vue/dist/Components/NcActionLink'
+
+export default {
+ name: 'AppMenu',
+ components: {
+ NcActions, NcActionLink,
+ },
+ data() {
+ return {
+ apps: loadState('core', 'apps', {}),
+ appLimit: 0,
+ }
+ },
+ computed: {
+ appList() {
+ return Object.values(this.apps)
+ },
+ mainAppList() {
+ return this.appList.slice(0, this.appLimit)
+ },
+ popoverAppList() {
+ return this.appList.slice(this.appLimit)
+ },
+ appLabel() {
+ return (app) => app.name
+ + (app.active ? ' (' + t('core', 'Currently open') + ')' : '')
+ + (app.unread > 0 ? ' (' + n('core', '{count} notification', '{count} notifications', app.unread, { count: app.unread }) + ')' : '')
+ },
+ },
+ mounted() {
+ window.addEventListener('resize', this.resize)
+ this.resize()
+ },
+ methods: {
+ setNavigationCounter(id, counter) {
+ this.$set(this.apps[id], 'unread', counter)
+ },
+ resize() {
+ const availableWidth = this.$el.offsetWidth
+ let appCount = Math.floor(availableWidth / 50) - 1
+ const popoverAppCount = this.appList.length - appCount
+ if (popoverAppCount === 1) {
+ appCount--
+ }
+ if (appCount < 1) {
+ appCount = 0
+ }
+ this.appLimit = appCount
+ },
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+$header-icon-size: 20px;
+
+.app-menu {
+ width: 100%;
+ display: flex;
+ flex-shrink: 1;
+ flex-wrap: wrap;
+}
+.app-menu-main {
+ display: flex;
+ flex-wrap: nowrap;
+
+ .app-menu-entry {
+ width: 50px;
+ height: 50px;
+ position: relative;
+ display: flex;
+ opacity: .7;
+
+ &.app-menu-entry__active {
+ opacity: 1;
+
+ &::before {
+ content: " ";
+ position: absolute;
+ pointer-events: none;
+ border: 8px solid transparent;
+ border-bottom-color: var(--color-main-background);
+ transform: translateX(-50%);
+ left: 50%;
+ bottom: 0;
+ display: block;
+ transition: all 0.1s ease-in-out;
+ opacity: 1;
+ }
+
+ .app-menu-entry--label {
+ font-weight: bold;
+ }
+ }
+
+ a {
+ width: 100%;
+ height: 100%;
+ color: var(--color-primary-text);
+ position: relative;
+ }
+
+ img {
+ transition: margin 0.1s ease-in-out;
+ width: $header-icon-size;
+ height: $header-icon-size;
+ padding: calc((100% - $header-icon-size) / 2);
+ filter: var(--primary-invert-if-bright);
+ }
+
+ .app-menu-entry--label {
+ opacity: 0;
+ position: absolute;
+ font-size: 12px;
+ color: var(--color-primary-text);
+ text-align: center;
+ bottom: -5px;
+ left: 50%;
+ display: block;
+ min-width: 100%;
+ transform: translateX(-50%);
+ transition: all 0.1s ease-in-out;
+ width: 100%;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ }
+
+ &:hover,
+ &:focus-within {
+ opacity: 1;
+ .app-menu-entry--label {
+ opacity: 1;
+ font-weight: bold;
+ font-size: 14px;
+ bottom: 0;
+ width: auto;
+ overflow: visible;
+ }
+ }
+
+ }
+
+ // Show labels
+ &:hover,
+ &:focus-within,
+ .app-menu-entry:hover,
+ .app-menu-entry:focus {
+ opacity: 1;
+
+ img {
+ margin-top: -6px;
+ }
+
+ .app-menu-entry--label {
+ opacity: 1;
+ bottom: 0;
+ }
+
+ &::before, .app-menu-entry::before {
+ border-width: 3px;
+ }
+ }
+}
+
+::v-deep .app-menu-more .button-vue--vue-tertiary {
+ color: var(--color-primary-text);
+ opacity: .7;
+ margin: 3px;
+
+ &:hover {
+ opacity: 1;
+ background-color: transparent !important;
+ }
+}
+
+.app-menu-popover-entry {
+ .app-icon {
+ position: relative;
+ height: 44px;
+
+ &.has-unread::after {
+ background-color: var(--color-main-text);
+ }
+
+ img {
+ filter: var(--background-invert-if-bright);
+ width: $header-icon-size;
+ height: $header-icon-size;
+ padding: calc((50px - $header-icon-size) / 2);
+ }
+ }
+}
+
+.has-unread::after {
+ content: "";
+ width: 8px;
+ height: 8px;
+ background-color: var(--color-primary-text);
+ border-radius: 50%;
+ position: absolute;
+ display: block;
+ top: 10px;
+ right: 10px;
+}
+
+.unread-counter {
+ display: none;
+}
+</style>