feat(webui): navigation drawer for sort/filter

preliminary to #283
This commit is contained in:
Gauthier Roebroeck 2020-08-25 15:35:05 +08:00
parent 9440654340
commit 28598cbef5
9 changed files with 290 additions and 146 deletions

View file

@ -0,0 +1,53 @@
<template>
<v-navigation-drawer
v-model="display"
right
fixed
temporary
disable-route-watcher
class="fill-height"
>
<slot></slot>
<template v-if="$slots.filter">
<v-divider/>
<v-subheader>FILTER</v-subheader>
</template>
<slot name="filter"></slot>
<template v-if="$slots.sort">
<v-divider/>
<v-subheader>SORT</v-subheader>
</template>
<slot name="sort"></slot>
</v-navigation-drawer>
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
name: 'FilterDrawer',
data: () => {
return {
display: false,
}
},
props: {
value: Boolean,
},
watch: {
value (val) {
this.display = val
},
display (val) {
!val && this.$emit('input', false)
},
},
})
</script>
<style scoped>
</style>

View file

@ -1,13 +1,5 @@
<template>
<v-menu offset-y>
<template v-slot:activator="{on}">
<v-btn icon v-on="on">
<v-icon :color="filterCustom ? 'secondary' : null"
>mdi-filter-variant
</v-icon>
</v-btn>
</template>
<v-list>
<v-list dense>
<div v-for="(f, key) in filtersOptions"
:key="key"
>
@ -29,26 +21,14 @@
</v-list-item-title>
</v-list-item>
</div>
<template v-if="filterCustom">
<v-divider/>
<v-list-item @click="clearAll" dense>
<v-list-item-icon>
<v-icon>mdi-close</v-icon>
</v-list-item-icon>
<v-list-item-title>Clear</v-list-item-title>
</v-list-item>
</template>
</v-list>
</v-menu>
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
name: 'FilterMenuButton',
name: 'FilterList',
props: {
filtersOptions: {
type: Object as () => FiltersOptions,
@ -59,24 +39,7 @@ export default Vue.extend({
required: true,
},
},
computed: {
filterCustom (): boolean {
let r = false
for (const [key, value] of Object.entries(this.filtersActive)) {
if (!this.$_.isEmpty(value)) r = true
}
return r
},
},
methods: {
clearAll () {
let r = this.$_.cloneDeep(this.filtersActive)
for (const key of Object.keys(r)) {
r[key] = []
}
this.$emit('update:filtersActive', r)
},
click (key: string, value: string) {
let r = this.$_.cloneDeep(this.filtersActive)
if (r[key].includes(value)) this.$_.pull(r[key], (value))

View file

@ -0,0 +1,86 @@
<template>
<v-expansion-panels accordion multiple flat tile hover>
<v-expansion-panel
v-for="(f, key) in filtersOptions"
:key="key"
>
<v-expansion-panel-header>
<v-icon
color="secondary"
style="max-width: 24px"
class="mr-2"
@click.stop="clear(key)"
>{{ groupActive(key) ? 'mdi-checkbox-marked' : '' }}
</v-icon>
{{ f.name }}
</v-expansion-panel-header>
<v-expansion-panel-content class="no-padding">
<v-list dense>
<v-list-item v-for="v in f.values"
:key="v"
@click.stop="click(key, v)"
>
<v-list-item-icon>
<v-icon v-if="filtersActive[key].includes(v)" color="secondary">
mdi-checkbox-marked
</v-icon>
<v-icon v-else>
mdi-checkbox-blank-outline
</v-icon>
</v-list-item-icon>
<v-list-item-title class="text-capitalize">
{{ v.toString().toLowerCase().replace('_', ' ') }}
</v-list-item-title>
</v-list-item>
</v-list>
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
name: 'FilterPanels',
props: {
filtersOptions: {
type: Object as () => FiltersOptions,
required: true,
},
filtersActive: {
type: Object as () => FiltersActive,
required: true,
},
},
methods: {
clear (key: string) {
let r = this.$_.cloneDeep(this.filtersActive)
r[key] = []
this.$emit('update:filtersActive', r)
},
groupActive (key: string): boolean {
for (let v of this.filtersOptions[key].values) {
if (this.filtersActive[key].includes(v)) {
return true
}
}
return false
},
click (key: string, value: string) {
let r = this.$_.cloneDeep(this.filtersActive)
if (r[key].includes(value)) this.$_.pull(r[key], (value))
else r[key].push(value)
this.$emit('update:filtersActive', r)
},
},
})
</script>
<style>
.no-padding .v-expansion-panel-content__wrap {
padding: 0;
}
</style>

View file

@ -0,0 +1,57 @@
<template>
<v-list dense>
<v-list-item v-for="(item, index) in sortOptions"
:key="index"
@click="setSort(item)"
>
<v-list-item-icon>
<v-icon color="secondary" v-if="item.key === sortActive.key && sortActive.order === 'asc'">
mdi-chevron-up
</v-icon>
<v-icon color="secondary" v-if="item.key === sortActive.key && sortActive.order === 'desc'">
mdi-chevron-down
</v-icon>
</v-list-item-icon>
<v-list-item-title>{{ item.name }}</v-list-item-title>
</v-list-item>
</v-list>
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
name: 'SortList',
props: {
sortOptions: {
type: Array,
required: true,
},
sortDefault: {
type: Object,
required: true,
},
sortActive: {
type: Object,
required: true,
},
},
methods: {
setSort (sort: SortOption) {
if (this.sortActive.key === sort.key) {
if (this.sortActive.order === 'desc') {
this.$emit('update:sortActive', { key: sort.key, order: 'asc' })
} else {
this.$emit('update:sortActive', { key: sort.key, order: 'desc' })
}
} else {
this.$emit('update:sortActive', { key: sort.key, order: 'desc' })
}
},
},
})
</script>
<style scoped>
</style>

View file

@ -1,71 +0,0 @@
<template>
<v-menu offset-y>
<template v-slot:activator="{on}">
<v-btn icon v-on="on">
<v-icon :color="sortCustom ? 'secondary' : null"
>mdi-sort-variant
</v-icon>
</v-btn>
</template>
<v-list>
<v-list-item v-for="(item, index) in sortOptions"
:key="index"
@click="setSort(item)"
>
<v-list-item-icon>
<v-icon color="secondary" v-if="item.key === sortActive.key && sortActive.order === 'asc'">
mdi-chevron-up
</v-icon>
<v-icon color="secondary" v-if="item.key === sortActive.key && sortActive.order === 'desc'">
mdi-chevron-down
</v-icon>
</v-list-item-icon>
<v-list-item-title>{{ item.name }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
name: 'SortMenuButton',
props: {
sortOptions: {
type: Array,
required: true,
},
sortDefault: {
type: Object,
required: true,
},
sortActive: {
type: Object,
required: true,
},
},
computed: {
sortCustom (): boolean {
return this.sortActive.key !== this.sortDefault.key || this.sortActive.order !== this.sortDefault.order
},
},
methods: {
setSort (sort: SortOption) {
if (this.sortActive.key === sort.key) {
if (this.sortActive.order === 'desc') {
this.$emit('update:sortActive', { key: sort.key, order: 'asc' })
} else {
this.$emit('update:sortActive', { key: sort.key, order: 'desc' })
}
} else {
this.$emit('update:sortActive', { key: sort.key, order: 'desc' })
}
},
},
})
</script>
<style scoped>
</style>

View file

@ -0,0 +1,5 @@
export function sortOrFilterActive (sortActive: SortActive, sortDefault: SortActive, filters: FiltersActive): boolean {
const sortCustom = sortActive.key !== sortDefault.key || sortActive.order !== sortDefault.order
const filterCustom = Object.keys(filters).some(x => filters[x].length !== 0)
return sortCustom || filterCustom
}

View file

@ -14,18 +14,11 @@
<v-spacer/>
<!-- Filter menu -->
<filter-menu-button :filters-options="filterOptions"
:filters-active.sync="filters"
/>
<!-- Sort menu -->
<sort-menu-button :sort-default="sortDefault"
:sort-options="sortOptions"
:sort-active.sync="sortActive"
/>
<page-size-select v-model="pageSize"/>
<v-btn icon @click="drawer = !drawer">
<v-icon :color="sortOrFilterActive ? 'secondary' : ''">mdi-filter-variant</v-icon>
</v-btn>
</toolbar-sticky>
<series-multi-select-bar
@ -39,11 +32,35 @@
<library-navigation :libraryId="libraryId"/>
<filter-drawer v-model="drawer">
<template v-slot:default>
<filter-list
:filters-options="filterOptionsList"
:filters-active.sync="filters"
/>
</template>
<template v-slot:filter>
<filter-panels
:filters-options="filterOptionsPanel"
:filters-active.sync="filters"
/>
</template>
<template v-slot:sort>
<sort-list
:sort-default="sortDefault"
:sort-options="sortOptions"
:sort-active.sync="sortActive"
/>
</template>
</filter-drawer>
<v-container fluid>
<empty-state
v-if="totalPages === 0"
title="The active filter has no matches"
sub-title="Use the menu above to change the active filter"
sub-title="Use the filter panel to change the active filter"
icon="mdi-book-multiple"
icon-color="secondary"
>
@ -72,12 +89,10 @@
import SeriesMultiSelectBar from '@/components/bars/SeriesMultiSelectBar.vue'
import ToolbarSticky from '@/components/bars/ToolbarSticky.vue'
import EmptyState from '@/components/EmptyState.vue'
import FilterMenuButton from '@/components/FilterMenuButton.vue'
import ItemBrowser from '@/components/ItemBrowser.vue'
import LibraryNavigation from '@/components/LibraryNavigation.vue'
import LibraryActionsMenu from '@/components/menus/LibraryActionsMenu.vue'
import PageSizeSelect from '@/components/PageSizeSelect.vue'
import SortMenuButton from '@/components/SortMenuButton.vue'
import { parseQueryFilter, parseQuerySort } from '@/functions/query-params'
import { ReadStatus } from '@/types/enum-books'
import { SeriesStatus } from '@/types/enum-series'
@ -85,6 +100,11 @@ import { LIBRARY_CHANGED, LIBRARY_DELETED, SERIES_CHANGED } from '@/types/events
import Vue from 'vue'
import { Location } from 'vue-router'
import { LIBRARIES_ALL } from '@/types/library'
import FilterDrawer from '@/components/FilterDrawer.vue'
import SortList from '@/components/SortList.vue'
import FilterPanels from '@/components/FilterPanels.vue'
import FilterList from '@/components/FilterList.vue'
import { sortOrFilterActive } from '@/functions/filter'
const cookiePageSize = 'pagesize'
@ -94,12 +114,14 @@ export default Vue.extend({
LibraryActionsMenu,
EmptyState,
ToolbarSticky,
SortMenuButton,
FilterMenuButton,
ItemBrowser,
PageSizeSelect,
LibraryNavigation,
SeriesMultiSelectBar,
FilterDrawer,
FilterPanels,
FilterList,
SortList,
},
data: () => {
return {
@ -117,10 +139,12 @@ export default Vue.extend({
] as SortOption[],
sortActive: {} as SortActive,
sortDefault: { key: 'metadata.titleSort', order: 'asc' } as SortActive,
filterOptions: {
filterOptionsList: {
readStatus: {
values: [ReadStatus.UNREAD],
},
} as FiltersOptions,
filterOptionsPanel: {
status: {
name: 'STATUS',
values: Object.values(SeriesStatus),
@ -131,6 +155,7 @@ export default Vue.extend({
filterUnwatch: null as any,
pageUnwatch: null as any,
pageSizeUnwatch: null as any,
drawer: false,
}
},
props: {
@ -208,6 +233,9 @@ export default Vue.extend({
return 15
}
},
sortOrFilterActive (): boolean {
return sortOrFilterActive(this.sortActive, this.sortDefault, this.filters)
},
},
methods: {
cookieSort (libraryId: string): string {

View file

@ -26,19 +26,11 @@
<v-icon>mdi-pencil</v-icon>
</v-btn>
<!-- Filter menu -->
<filter-menu-button :filters-options="filterOptions"
:filters-active.sync="filters"
/>
<!-- Sort menu -->
<sort-menu-button :sort-default="sortDefault"
:sort-options="sortOptions"
:sort-active.sync="sortActive"
/>
<page-size-select v-model="pageSize"/>
<v-btn icon @click="drawer = !drawer">
<v-icon :color="sortOrFilterActive ? 'secondary' : ''">mdi-filter-variant</v-icon>
</v-btn>
</toolbar-sticky>
<books-multi-select-bar
@ -50,6 +42,30 @@
@edit="editMultipleBooks"
/>
<filter-drawer v-model="drawer">
<template v-slot:default>
<filter-list
:filters-options="filterOptionsList"
:filters-active.sync="filters"
/>
</template>
<!-- <template v-slot:filter>-->
<!-- <filter-panels-->
<!-- :filters-options="filterOptionsPanels"-->
<!-- :filters-active.sync="filters"-->
<!-- />-->
<!-- </template>-->
<template v-slot:sort>
<sort-list
:sort-default="sortDefault"
:sort-options="sortOptions"
:sort-active.sync="sortActive"
/>
</template>
</filter-drawer>
<v-container fluid>
<v-row>
<v-col cols="4" sm="4" md="auto" lg="auto" xl="auto">
@ -149,7 +165,7 @@
<empty-state
v-if="totalPages === 0"
title="The active filter has no matches"
sub-title="Use the menu above to change the active filter"
sub-title="Use the filter panel to change the active filter"
icon="mdi-book-multiple"
icon-color="secondary"
>
@ -179,12 +195,10 @@ import BooksMultiSelectBar from '@/components/bars/BooksMultiSelectBar.vue'
import ToolbarSticky from '@/components/bars/ToolbarSticky.vue'
import CollectionsExpansionPanels from '@/components/CollectionsExpansionPanels.vue'
import EmptyState from '@/components/EmptyState.vue'
import FilterMenuButton from '@/components/FilterMenuButton.vue'
import ItemBrowser from '@/components/ItemBrowser.vue'
import ItemCard from '@/components/ItemCard.vue'
import SeriesActionsMenu from '@/components/menus/SeriesActionsMenu.vue'
import PageSizeSelect from '@/components/PageSizeSelect.vue'
import SortMenuButton from '@/components/SortMenuButton.vue'
import { parseQueryFilter, parseQuerySort } from '@/functions/query-params'
import { seriesThumbnailUrl } from '@/functions/urls'
import { ReadStatus } from '@/types/enum-books'
@ -192,6 +206,10 @@ import { BOOK_CHANGED, LIBRARY_DELETED, READLIST_CHANGED, SERIES_CHANGED } from
import Vue from 'vue'
import { Location } from 'vue-router'
import { SeriesStatus } from '@/types/enum-series'
import FilterDrawer from '@/components/FilterDrawer.vue'
import FilterList from '@/components/FilterList.vue'
import SortList from '@/components/SortList.vue'
import { sortOrFilterActive } from '@/functions/filter'
const tags = require('language-tags')
@ -201,8 +219,6 @@ export default Vue.extend({
name: 'BrowseSeries',
components: {
ToolbarSticky,
SortMenuButton,
FilterMenuButton,
ItemBrowser,
PageSizeSelect,
SeriesActionsMenu,
@ -210,6 +226,9 @@ export default Vue.extend({
EmptyState,
BooksMultiSelectBar,
CollectionsExpansionPanels,
FilterDrawer,
FilterList,
SortList,
},
data: () => {
return {
@ -226,7 +245,7 @@ export default Vue.extend({
}] as SortOption[],
sortActive: {} as SortActive,
sortDefault: { key: 'metadata.numberSort', order: 'asc' } as SortActive,
filterOptions: {
filterOptionsList: {
readStatus: {
values: [ReadStatus.UNREAD],
},
@ -237,6 +256,7 @@ export default Vue.extend({
pageUnwatch: null as any,
pageSizeUnwatch: null as any,
collections: [] as CollectionDto[],
drawer: false,
}
},
computed: {
@ -276,6 +296,9 @@ export default Vue.extend({
}
return { color: undefined, text: undefined }
},
sortOrFilterActive (): boolean {
return sortOrFilterActive(this.sortActive, this.sortDefault, this.filters)
},
},
props: {
seriesId: {

View file

@ -1,5 +1,5 @@
<template>
<div>
<div class="fill-height">
<v-app-bar
app
>
@ -125,7 +125,7 @@
</template>
</v-navigation-drawer>
<v-main>
<v-main class="fill-height">
<dialogs/>
<router-view/>
</v-main>