4b3f92ae34
Make backgrounds list responsive Show initial collection of backgrounds Remove test data Fix "backgroundInformation is null" when navigating Fix kanban height Remove debug log Move list title to top header Add styling for title in top header Set the current list (and background) when loading settings Only load the background if it changed Make task detail view look good again Fix bottom spacing Make list and table view look good again Make pages with background at least 100vh Fix kanban height Make extra buttons look good again Move list title and view-switcher in one row Add styling for backgrounds Set background globally Add getting list background and putting it in vuex Add setting list background Move list background setting to seperate list Add search timeout to not search on every keypress Add getting thumbnails through api Add basic search for unsplash backgrounds Co-authored-by: kolaente <k@knt.li> Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/144
391 lines
13 KiB
Vue
391 lines
13 KiB
Vue
<template>
|
|
<div>
|
|
<div v-if="online">
|
|
<!-- This is a workaround to get the sw to "see" the to-be-cached version of the offline background image -->
|
|
<div class="offline" style="height: 0;width: 0;"></div>
|
|
<nav
|
|
class="navbar main-theme is-fixed-top"
|
|
:class="{'has-background': background}"
|
|
role="navigation"
|
|
aria-label="main navigation"
|
|
v-if="userAuthenticated && (userInfo && userInfo.type === authTypes.USER)">
|
|
<div class="navbar-brand">
|
|
<router-link :to="{name: 'home'}" class="navbar-item logo">
|
|
<img src="/images/logo-full.svg" alt="Vikunja"/>
|
|
</router-link>
|
|
</div>
|
|
<div class="list-title" v-if="currentList.id">
|
|
<h1
|
|
class="title"
|
|
:style="{ 'opacity': currentList.title === '' ? '0': '1' }">
|
|
{{ currentList.title === '' ? 'Loading...': currentList.title}}
|
|
</h1>
|
|
<router-link :to="{ name: 'editList', params: { id: currentList.id } }" class="icon">
|
|
<icon icon="cog" size="2x"/>
|
|
</router-link>
|
|
</div>
|
|
<div class="navbar-end">
|
|
<div v-if="updateAvailable" class="update-notification">
|
|
<p>There is an update for Vikunja available!</p>
|
|
<a @click="refreshApp()" class="button is-primary noshadow">Update Now</a>
|
|
</div>
|
|
<div class="user">
|
|
<img :src="userInfo.getAvatarUrl()" class="avatar" alt=""/>
|
|
<div class="dropdown is-right is-active">
|
|
<div class="dropdown-trigger">
|
|
<button class="button noshadow" @click="userMenuActive = !userMenuActive">
|
|
<span class="username">{{userInfo.username}}</span>
|
|
<span class="icon is-small">
|
|
<icon icon="chevron-down"/>
|
|
</span>
|
|
</button>
|
|
</div>
|
|
<transition name="fade">
|
|
<div class="dropdown-menu" v-if="userMenuActive">
|
|
<div class="dropdown-content">
|
|
<router-link :to="{name: 'userSettings'}" class="dropdown-item">
|
|
Settings
|
|
</router-link>
|
|
<a @click="logout()" class="dropdown-item">
|
|
Logout
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</transition>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
<div v-if="userAuthenticated && (userInfo && userInfo.type === authTypes.USER)">
|
|
<a @click="mobileMenuActive = true" class="mobilemenu-show-button" v-if="!mobileMenuActive">
|
|
<icon icon="bars"></icon>
|
|
</a>
|
|
<a @click="mobileMenuActive = false" class="mobilemenu-hide-button" v-if="mobileMenuActive">
|
|
<icon icon="times"></icon>
|
|
</a>
|
|
<div
|
|
class="app-container"
|
|
:class="{'has-background': background}"
|
|
:style="{'background-image': `url(${background})`}"
|
|
>
|
|
<div class="namespace-container" :class="{'is-active': mobileMenuActive}">
|
|
<div class="menu top-menu">
|
|
<router-link :to="{name: 'home'}" class="logo">
|
|
<img src="/images/logo-full.svg" alt="Vikunja"/>
|
|
</router-link>
|
|
<ul class="menu-list">
|
|
<li>
|
|
<router-link :to="{ name: 'home'}">
|
|
<span class="icon">
|
|
<icon icon="calendar"/>
|
|
</span>
|
|
Overview
|
|
</router-link>
|
|
</li>
|
|
<li>
|
|
<router-link :to="{ name: 'showTasksInRange', params: {type: 'week'}}">
|
|
<span class="icon">
|
|
<icon icon="calendar-week"/>
|
|
</span>
|
|
Next Week
|
|
</router-link>
|
|
</li>
|
|
<li>
|
|
<router-link :to="{ name: 'showTasksInRange', params: {type: 'month'}}">
|
|
<span class="icon">
|
|
<icon :icon="['far', 'calendar-alt']"/>
|
|
</span>
|
|
Next Month
|
|
</router-link>
|
|
</li>
|
|
<li>
|
|
<router-link :to="{ name: 'listTeams'}">
|
|
<span class="icon">
|
|
<icon icon="users"/>
|
|
</span>
|
|
Teams
|
|
</router-link>
|
|
</li>
|
|
<li>
|
|
<router-link :to="{ name: 'newNamespace'}">
|
|
<span class="icon">
|
|
<icon icon="layer-group"/>
|
|
</span>
|
|
New Namespace
|
|
</router-link>
|
|
</li>
|
|
<li>
|
|
<router-link :to="{ name: 'listLabels'}">
|
|
<span class="icon">
|
|
<icon icon="tags"/>
|
|
</span>
|
|
Labels
|
|
</router-link>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<aside class="menu namespaces-lists">
|
|
<fancycheckbox v-model="showArchived" class="show-archived-check">
|
|
Show Archived
|
|
</fancycheckbox>
|
|
<div class="spinner" :class="{ 'is-loading': namespaceService.loading}"></div>
|
|
<template v-for="n in namespaces">
|
|
<div :key="n.id">
|
|
<router-link v-tooltip.right="'Settings'"
|
|
:to="{name: 'editNamespace', params: {id: n.id} }" class="nsettings"
|
|
v-if="n.id > 0">
|
|
<span class="icon">
|
|
<icon icon="cog"/>
|
|
</span>
|
|
</router-link>
|
|
<router-link v-tooltip="'Add a new list in the ' + n.title + ' namespace'"
|
|
:to="{ name: 'newList', params: { id: n.id} }" class="nsettings"
|
|
:key="n.id + 'newList'" v-if="n.id > 0">
|
|
<span class="icon">
|
|
<icon icon="plus"/>
|
|
</span>
|
|
</router-link>
|
|
<label class="menu-label" v-tooltip="n.title + ' (' + n.lists.length + ')'" :for="n.id + 'checker'">
|
|
<span class="name">
|
|
<span class="color-bubble" v-if="n.hexColor !== ''" :style="{ backgroundColor: n.hexColor }"></span>
|
|
{{n.title}} ({{n.lists.length}})
|
|
</span>
|
|
<span class="is-archived" v-if="n.isArchived">
|
|
Archived
|
|
</span>
|
|
</label>
|
|
</div>
|
|
<input :key="n.id + 'checker'" type="checkbox" checked="checked" :id="n.id + 'checker'" class="checkinput"/>
|
|
<div class="more-container" :key="n.id + 'child'">
|
|
<ul class="menu-list can-be-hidden" >
|
|
<li v-for="l in n.lists" :key="l.id">
|
|
<router-link :to="{ name: 'list.index', params: { listId: l.id} }" :class="{'router-link-exact-active': currentList.id === l.id}">
|
|
<span class="name">
|
|
<span class="color-bubble" v-if="l.hexColor !== ''" :style="{ backgroundColor: l.hexColor }"></span>
|
|
{{l.title}}
|
|
</span>
|
|
<span class="is-archived" v-if="l.isArchived">
|
|
Archived
|
|
</span>
|
|
</router-link>
|
|
</li>
|
|
</ul>
|
|
<label class="hidden-hint" :for="n.id + 'checker'">
|
|
Show hidden lists ({{n.lists.length}})...
|
|
</label>
|
|
</div>
|
|
</template>
|
|
</aside>
|
|
<a class="menu-bottom-link" target="_blank" href="https://vikunja.io">Powered by Vikunja</a>
|
|
</div>
|
|
<div class="app-content" :class="{'fullpage-overlay': fullpage}">
|
|
<a class="mobile-overlay" v-if="mobileMenuActive" @click="mobileMenuActive = false"></a>
|
|
<transition name="fade">
|
|
<router-view/>
|
|
</transition>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-else-if="userAuthenticated && (userInfo && userInfo.type === authTypes.LINK_SHARE)">
|
|
<div class="container has-text-centered link-share-view">
|
|
<div class="column is-10 is-offset-1">
|
|
<img src="/images/logo-full.svg" alt="Vikunja" class="logo"/>
|
|
<div class="box has-text-left">
|
|
<div class="logout">
|
|
<a @click="logout()" class="button logout">
|
|
<span>Logout</span>
|
|
<span class="icon is-small">
|
|
<icon icon="sign-out-alt"/>
|
|
</span>
|
|
</a>
|
|
</div>
|
|
<router-view/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-else>
|
|
<div class="noauth-container">
|
|
<img src="/images/logo-full.svg" alt="Vikunja"/>
|
|
<div class="message is-info" v-if="motd !== ''">
|
|
<div class="message-header">
|
|
<p>Info</p>
|
|
</div>
|
|
<div class="message-body">
|
|
{{ motd }}
|
|
</div>
|
|
</div>
|
|
<router-view/>
|
|
</div>
|
|
</div>
|
|
<notification/>
|
|
</div>
|
|
<div class="app offline" v-else>
|
|
<div class="offline-message">
|
|
<h1>You are offline.</h1>
|
|
<p>Please check your network connection and try again.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import router from './router'
|
|
import {mapState} from 'vuex'
|
|
|
|
import NamespaceService from './services/namespace'
|
|
import authTypes from './models/authTypes'
|
|
|
|
import swEvents from './ServiceWorker/events'
|
|
import Notification from './components/global/notification'
|
|
import Fancycheckbox from './components/global/fancycheckbox'
|
|
import {CURRENT_LIST, IS_FULLPAGE, ONLINE} from './store/mutation-types'
|
|
|
|
export default {
|
|
name: 'app',
|
|
components: {
|
|
Fancycheckbox,
|
|
Notification,
|
|
},
|
|
data() {
|
|
return {
|
|
namespaceService: NamespaceService,
|
|
mobileMenuActive: false,
|
|
currentDate: new Date(),
|
|
userMenuActive: false,
|
|
authTypes: authTypes,
|
|
showArchived: false,
|
|
|
|
// Service Worker stuff
|
|
updateAvailable: false,
|
|
registration: null,
|
|
refreshing: false,
|
|
}
|
|
},
|
|
beforeMount() {
|
|
// Check if the user is offline, show a message then
|
|
this.$store.commit(ONLINE, navigator.onLine)
|
|
window.addEventListener('online', () => this.$store.commit(ONLINE, navigator.onLine));
|
|
window.addEventListener('offline', () => this.$store.commit(ONLINE, navigator.onLine));
|
|
|
|
// Password reset
|
|
if (this.$route.query.userPasswordReset !== undefined) {
|
|
localStorage.removeItem('passwordResetToken') // Delete an eventually preexisting old token
|
|
localStorage.setItem('passwordResetToken', this.$route.query.userPasswordReset)
|
|
router.push({name: 'passwordReset'})
|
|
}
|
|
// Email verification
|
|
if (this.$route.query.userEmailConfirm !== undefined) {
|
|
localStorage.removeItem('emailConfirmToken') // Delete an eventually preexisting old token
|
|
localStorage.setItem('emailConfirmToken', this.$route.query.userEmailConfirm)
|
|
router.push({name: 'login'})
|
|
}
|
|
},
|
|
beforeCreate() {
|
|
this.$store.dispatch('config/update')
|
|
this.$store.dispatch('auth/checkAuth')
|
|
.then(() => {
|
|
// Check if the user is already logged in, if so, redirect them to the homepage
|
|
if (
|
|
!this.userAuthenticated &&
|
|
this.$route.name !== 'login' &&
|
|
this.$route.name !== 'getPasswordReset' &&
|
|
this.$route.name !== 'passwordReset' &&
|
|
this.$route.name !== 'register' &&
|
|
this.$route.name !== 'linkShareAuth'
|
|
) {
|
|
router.push({name: 'login'})
|
|
}
|
|
|
|
if (this.userAuthenticated && this.userInfo.type === authTypes.USER && (this.$route.params.name === 'home' || this.namespaces.length === 0)) {
|
|
this.loadNamespaces()
|
|
}
|
|
})
|
|
},
|
|
created() {
|
|
|
|
// Service worker communication
|
|
document.addEventListener(swEvents.SW_UPDATED, this.showRefreshUI, {once: true})
|
|
|
|
navigator.serviceWorker.addEventListener(
|
|
'controllerchange', () => {
|
|
if (this.refreshing) return;
|
|
this.refreshing = true;
|
|
window.location.reload();
|
|
}
|
|
);
|
|
|
|
// Schedule a token renew every minute
|
|
setTimeout(() => {
|
|
this.$store.dispatch('auth/renewToken')
|
|
}, 1000 * 60)
|
|
},
|
|
watch: {
|
|
// call the method again if the route changes
|
|
'$route': 'doStuffAfterRoute',
|
|
},
|
|
computed: mapState({
|
|
userInfo: state => state.auth.info,
|
|
userAuthenticated: state => state.auth.authenticated,
|
|
motd: state => state.config.motd,
|
|
online: ONLINE,
|
|
fullpage: IS_FULLPAGE,
|
|
namespaces(state) {
|
|
return state.namespaces.namespaces.filter(n => this.showArchived ? true : !n.isArchived)
|
|
},
|
|
currentList: CURRENT_LIST,
|
|
background: 'background',
|
|
}),
|
|
methods: {
|
|
logout() {
|
|
this.$store.dispatch('auth/logout')
|
|
router.push({name: 'login'})
|
|
},
|
|
loadNamespaces() {
|
|
this.$store.dispatch('namespaces/loadNamespaces')
|
|
},
|
|
loadNamespacesIfNeeded(e) {
|
|
if (this.userAuthenticated && (this.userInfo && this.userInfo.type === authTypes.USER) && (e.name === 'home' || this.namespaces.length === 0)) {
|
|
this.loadNamespaces()
|
|
}
|
|
},
|
|
doStuffAfterRoute(e) {
|
|
if (this.$store.state[IS_FULLPAGE]) {
|
|
this.$store.commit(IS_FULLPAGE, false)
|
|
}
|
|
|
|
this.loadNamespacesIfNeeded(e)
|
|
this.mobileMenuActive = false
|
|
this.userMenuActive = false
|
|
|
|
// Reset the current list highlight in menu if the current list is not list related.
|
|
if (
|
|
this.$route.name === 'home' ||
|
|
this.$route.name === 'editNamespace' ||
|
|
this.$route.name === 'listTeams' ||
|
|
this.$route.name === 'editTeam' ||
|
|
this.$route.name === 'showTasksInRange' ||
|
|
this.$route.name === 'listLabels' ||
|
|
this.$route.name === 'migrateStart' ||
|
|
this.$route.name === 'migrate.wunderlist' ||
|
|
this.$route.name === 'userSettings'
|
|
) {
|
|
this.$store.commit(CURRENT_LIST, {})
|
|
}
|
|
},
|
|
showRefreshUI(e) {
|
|
console.log('recieved refresh event', e)
|
|
this.registration = e.detail;
|
|
this.updateAvailable = true;
|
|
},
|
|
refreshApp() {
|
|
this.updateExists = false;
|
|
if (!this.registration || !this.registration.waiting) {
|
|
return;
|
|
}
|
|
// Notify the service worker to actually do the update
|
|
this.registration.waiting.postMessage('skipWaiting');
|
|
},
|
|
},
|
|
}
|
|
</script>
|