feat(webui): action menu on item cards

This commit is contained in:
Gauthier Roebroeck 2020-06-26 10:49:18 +08:00
parent 08919814d3
commit 37d790d1fc
16 changed files with 294 additions and 57 deletions

View file

@ -1,6 +1,6 @@
<template>
<div>
<v-menu offset-y>
<v-menu offset-y v-model="menuState">
<template v-slot:activator="{ on }">
<v-btn icon v-on="on" @click.prevent="">
<v-icon>mdi-dots-vertical</v-icon>
@ -26,15 +26,30 @@
<script lang="ts">
import { getReadProgress } from '@/functions/book-progress'
import { ReadStatus } from '@/types/enum-books'
import { BOOK_CHANGED } from '@/types/events'
import Vue from 'vue'
export default Vue.extend({
name: 'BookActionsMenu',
data: () => {
return {
menuState: false,
}
},
props: {
book: {
type: Object as () => BookDto,
required: true,
},
menu: {
type: Boolean,
default: false,
},
},
watch: {
menuState (val) {
this.$emit('update:menu', val)
},
},
computed: {
isAdmin (): boolean {
@ -57,11 +72,17 @@ export default Vue.extend({
async markRead () {
const readProgress = { completed: true } as ReadProgressUpdateDto
await this.$komgaBooks.updateReadProgress(this.book.id, readProgress)
this.$emit('mark-read', true)
this.$eventHub.$emit(BOOK_CHANGED, {
id: this.book.id,
seriesId: this.book.seriesId,
} as EventBookChanged)
},
async markUnread () {
await this.$komgaBooks.deleteReadProgress(this.book.id)
this.$emit('mark-unread', true)
this.$eventHub.$emit(BOOK_CHANGED, {
id: this.book.id,
seriesId: this.book.seriesId,
} as EventBookChanged)
},
},
})

View file

@ -1,6 +1,6 @@
<template>
<div>
<v-menu offset-y v-if="isAdmin">
<v-menu offset-y v-if="isAdmin" v-model="menuState">
<template v-slot:activator="{ on }">
<v-btn icon v-on="on" @click.prevent="">
<v-icon>mdi-dots-vertical</v-icon>
@ -13,23 +13,16 @@
</v-list-item>
</v-list>
</v-menu>
<collection-delete-dialog v-model="modalDeleteCollection"
:collection="collection"
@deleted="afterDelete"
/>
</div>
</template>
<script lang="ts">
import CollectionDeleteDialog from '@/components/CollectionDeleteDialog.vue'
import Vue from 'vue'
export default Vue.extend({
name: 'CollectionActionsMenu',
components: { CollectionDeleteDialog },
data: function () {
return {
modalDeleteCollection: false,
menuState: false,
}
},
props: {
@ -37,6 +30,15 @@ export default Vue.extend({
type: Object as () => CollectionDto,
required: true,
},
menu: {
type: Boolean,
default: false,
},
},
watch: {
menuState (val) {
this.$emit('update:menu', val)
},
},
computed: {
isAdmin (): boolean {
@ -45,10 +47,7 @@ export default Vue.extend({
},
methods: {
promptDeleteCollection () {
this.modalDeleteCollection = true
},
afterDelete () {
this.$emit('deleted', true)
this.$store.dispatch('dialogDeleteCollection', this.collection)
},
},
})

View file

@ -0,0 +1,76 @@
<template>
<div>
<collection-add-to-dialog
v-model="addToCollectionDialog"
:series="addToCollectionSeries"
@added="collectionAdded"
/>
<collection-delete-dialog
v-model="deleteCollectionDialog"
:collection="deleteCollection"
@deleted="collectionDeleted"
/>
</div>
</template>
<script lang="ts">
import CollectionAddToDialog from '@/components/CollectionAddToDialog.vue'
import CollectionDeleteDialog from '@/components/CollectionDeleteDialog.vue'
import { COLLECTION_CHANGED, SERIES_CHANGED } from '@/types/events'
import Vue from 'vue'
export default Vue.extend({
name: 'Dialogs',
components: { CollectionAddToDialog, CollectionDeleteDialog },
computed: {
addToCollectionDialog: {
get (): boolean {
return this.$store.state.addToCollectionDialog
},
set (val) {
this.$store.dispatch('dialogAddSeriesToCollectionDisplay', val)
},
},
addToCollectionSeries (): SeriesDto | SeriesDto[] {
return this.$store.state.addToCollectionSeries
},
deleteCollectionDialog: {
get (): boolean {
return this.$store.state.deleteCollectionDialog
},
set (val) {
this.$store.dispatch('dialogDeleteCollectionDisplay', val)
},
},
deleteCollection (): CollectionDto {
return this.$store.state.deleteCollection
},
},
methods: {
collectionAdded () {
if (Array.isArray(this.addToCollectionSeries)) {
this.addToCollectionSeries.forEach(s => {
this.$eventHub.$emit(SERIES_CHANGED, {
id: s.id,
libraryId: s.libraryId,
} as EventSeriesChanged)
})
} else {
this.$eventHub.$emit(SERIES_CHANGED, {
id: this.addToCollectionSeries.id,
libraryId: this.addToCollectionSeries.libraryId,
} as EventSeriesChanged)
}
this.$eventHub.$emit(COLLECTION_CHANGED)
},
collectionDeleted () {
this.$eventHub.$emit(COLLECTION_CHANGED)
},
},
})
</script>
<style scoped>
</style>

View file

@ -28,10 +28,10 @@
<!-- Thumbnail overlay -->
<v-fade-transition>
<v-overlay
v-if="hover || selected || preselect"
v-if="hover || selected || preselect || actionMenuState"
absolute
:opacity="hover ? 0.3 : 0"
:class="`${hover ? 'item-border-darken' : selected ? 'item-border' : 'item-border-transparent'} overlay-full`"
:opacity="hover || actionMenuState ? 0.3 : 0"
:class="`${hover || actionMenuState ? 'item-border-darken' : selected ? 'item-border' : 'item-border-transparent'} overlay-full`"
>
<!-- Circle icon for selection (top left) -->
<v-icon v-if="onSelected"
@ -56,12 +56,31 @@
</v-btn>
<!-- Pen icon for edition (bottom left) -->
<v-icon v-if="!selected && !preselect && onEdit"
style="position: absolute; bottom: 10px; left: 10px"
@click.stop="editItem"
<v-btn icon
v-if="!selected && !preselect && onEdit"
style="position: absolute; bottom: 5px; left: 5px"
@click.stop="editItem"
>
mdi-pencil
</v-icon>
<v-icon>mdi-pencil</v-icon>
</v-btn>
<!-- Action menu (bottom right) -->
<div v-if="!selected && !preselect && actionMenu"
style="position: absolute; bottom: 5px; right: 5px"
>
<book-actions-menu v-if="computedItem.type() === ItemTypes.BOOK"
:book="item"
:menu.sync="actionMenuState"
/>
<series-actions-menu v-if="computedItem.type() === ItemTypes.SERIES"
:series="item"
:menu.sync="actionMenuState"
/>
<collection-actions-menu v-if="computedItem.type() === ItemTypes.COLLECTION"
:collection="item"
:menu.sync="actionMenuState"
/>
</div>
</v-overlay>
</v-fade-transition>
<v-progress-linear
@ -90,6 +109,9 @@
</template>
<script lang="ts">
import BookActionsMenu from '@/components/BookActionsMenu.vue'
import CollectionActionsMenu from '@/components/CollectionActionsMenu.vue'
import SeriesActionsMenu from '@/components/SeriesActionsMenu.vue'
import { getReadProgress, getReadProgressPercentage } from '@/functions/book-progress'
import { ReadStatus } from '@/types/enum-books'
import { createItem, Item, ItemTypes } from '@/types/items'
@ -97,6 +119,7 @@ import Vue from 'vue'
export default Vue.extend({
name: 'ItemCard',
components: { BookActionsMenu, SeriesActionsMenu, CollectionActionsMenu },
props: {
item: {
type: Object as () => BookDto | SeriesDto | CollectionDto,
@ -139,16 +162,24 @@ export default Vue.extend({
default: undefined,
required: false,
},
// action menu enabled or not
actionMenu: {
type: Boolean,
default: true,
},
},
data: () => {
return {}
return {
ItemTypes,
actionMenuState: false,
}
},
computed: {
canReadPages (): boolean {
return this.$store.getters.mePageStreaming && this.computedItem.type() === ItemTypes.BOOK
},
overlay (): boolean {
return this.onEdit !== undefined || this.onSelected !== undefined || this.bookReady || this.canReadPages
return this.onEdit !== undefined || this.onSelected !== undefined || this.bookReady || this.canReadPages || this.actionMenu
},
computedItem (): Item<BookDto | SeriesDto | CollectionDto> {
return createItem(this.item)

View file

@ -1,6 +1,6 @@
<template>
<div>
<v-menu offset-y>
<v-menu offset-y v-model="menuState">
<template v-slot:activator="{ on }">
<v-btn icon v-on="on" @click.prevent="">
<v-icon>mdi-dots-vertical</v-icon>
@ -27,15 +27,30 @@
</div>
</template>
<script lang="ts">
import { SERIES_CHANGED } from '@/types/events'
import Vue from 'vue'
export default Vue.extend({
name: 'SeriesActionsMenu',
data: () => {
return {
menuState: false,
}
},
props: {
series: {
type: Object as () => SeriesDto,
required: true,
},
menu: {
type: Boolean,
default: false,
},
},
watch: {
menuState (val) {
this.$emit('update:menu', val)
},
},
computed: {
isAdmin (): boolean {
@ -56,15 +71,21 @@ export default Vue.extend({
this.$komgaSeries.refreshMetadata(this.series)
},
addToCollection () {
this.$emit('add-to-collection', true)
this.$store.dispatch('dialogAddSeriesToCollection', this.series)
},
async markRead () {
await this.$komgaSeries.markAsRead(this.series.id)
this.$emit('mark-read', true)
this.$eventHub.$emit(SERIES_CHANGED, {
id: this.series.id,
libraryId: this.series.libraryId,
} as EventSeriesChanged)
},
async markUnread () {
await this.$komgaSeries.markAsUnread(this.series.id)
this.$emit('mark-unread', true)
this.$eventHub.$emit(SERIES_CHANGED, {
id: this.series.id,
libraryId: this.series.libraryId,
} as EventSeriesChanged)
},
},
})

View file

@ -36,6 +36,7 @@ Vue.use(komgaLibraries, { store: store, http: Vue.prototype.$http })
Vue.use(actuator, { http: Vue.prototype.$http })
Vue.prototype.$_ = _
Vue.prototype.$eventHub = new Vue()
Vue.config.productionTip = false
@ -51,6 +52,7 @@ new Vue({
declare module 'vue/types/vue' {
interface Vue {
$_: LoDashStatic;
$eventHub: Vue;
}
}

View file

@ -4,7 +4,40 @@ import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {},
mutations: {},
actions: {},
state: {
addToCollectionSeries: {} as SeriesDto | SeriesDto[],
addToCollectionDialog: false,
deleteCollection: {} as CollectionDto,
deleteCollectionDialog: false,
},
mutations: {
setAddToCollectionSeries (state, series) {
state.addToCollectionSeries = series
},
setAddToCollectionDialog (state, dialog) {
state.addToCollectionDialog = dialog
},
setDeleteCollection (state, collection) {
state.deleteCollection = collection
},
setDeleteCollectionDialog (state, dialog) {
state.deleteCollectionDialog = dialog
},
},
actions: {
dialogAddSeriesToCollection ({ commit }, series) {
commit('setAddToCollectionSeries', series)
commit('setAddToCollectionDialog', true)
},
dialogAddSeriesToCollectionDisplay ({ commit }, value) {
commit('setAddToCollectionDialog', value)
},
dialogDeleteCollection ({ commit }, collection) {
commit('setDeleteCollection', collection)
commit('setDeleteCollectionDialog', true)
},
dialogDeleteCollectionDisplay ({ commit }, value) {
commit('setDeleteCollectionDialog', value)
},
},
})

View file

@ -0,0 +1,9 @@
interface EventBookChanged {
id: number,
seriesId: number
}
interface EventSeriesChanged {
id: number,
libraryId: number
}

View file

@ -0,0 +1,3 @@
export const BOOK_CHANGED = 'book-changed'
export const SERIES_CHANGED = 'series-changed'
export const COLLECTION_CHANGED = 'collection-changed'

View file

@ -253,7 +253,6 @@ import SettingsSelect from '@/components/SettingsSelect.vue'
import SettingsSwitch from '@/components/SettingsSwitch.vue'
import ThumbnailExplorerDialog from '@/components/ThumbnailExplorerDialog.vue'
import { getBookTitleCompact } from '@/functions/book-title'
import { checkWebpFeature } from '@/functions/check-webp'
import { bookPageUrl } from '@/functions/urls'
import { ReadingDirection } from '@/types/enum-books'

View file

@ -18,8 +18,6 @@
<!-- Action menu -->
<book-actions-menu v-if="book"
:book="book"
@mark-read="loadBook(bookId)"
@mark-unread="loadBook(bookId)"
/>
</toolbar-sticky>
@ -32,6 +30,7 @@
:item="book"
thumbnail-only
no-link
:action-menu="false"
></item-card>
</v-col>
@ -158,6 +157,7 @@ import { getReadProgress, getReadProgressPercentage } from '@/functions/book-pro
import { getBookTitleCompact } from '@/functions/book-title'
import { bookFileUrl, bookThumbnailUrl } from '@/functions/urls'
import { ReadStatus } from '@/types/enum-books'
import { BOOK_CHANGED } from '@/types/events'
import Vue from 'vue'
export default Vue.extend({
@ -172,6 +172,10 @@ export default Vue.extend({
},
async created () {
this.loadBook(this.bookId)
this.$eventHub.$on(BOOK_CHANGED, this.reloadBook)
},
beforeDestroy () {
this.$eventHub.$off(BOOK_CHANGED, this.reloadBook)
},
watch: {
async book (val) {
@ -230,6 +234,9 @@ export default Vue.extend({
},
},
methods: {
reloadBook (event: EventBookChanged) {
if (event.id === this.bookId) this.loadBook(this.bookId)
},
async loadBook (bookId: number) {
this.book = await this.$komgaBooks.getBook(bookId)
},

View file

@ -4,7 +4,6 @@
<collection-actions-menu v-if="collection"
:collection="collection"
@deleted="afterDelete"
/>
<v-toolbar-title v-if="collection">
@ -141,6 +140,7 @@ import CollectionEditDialog from '@/components/CollectionEditDialog.vue'
import EditSeriesDialog from '@/components/EditSeriesDialog.vue'
import ItemBrowser from '@/components/ItemBrowser.vue'
import ToolbarSticky from '@/components/ToolbarSticky.vue'
import { COLLECTION_CHANGED } from '@/types/events'
import Vue from 'vue'
export default Vue.extend({
@ -203,6 +203,12 @@ export default Vue.extend({
}
},
},
created () {
this.$eventHub.$on(COLLECTION_CHANGED, this.afterDelete)
},
beforeDestroy () {
this.$eventHub.$off(COLLECTION_CHANGED, this.afterDelete)
},
mounted () {
this.loadCollection(this.collectionId)
},

View file

@ -31,6 +31,7 @@ import ItemBrowser from '@/components/ItemBrowser.vue'
import LibraryActionsMenu from '@/components/LibraryActionsMenu.vue'
import LibraryNavigation from '@/components/LibraryNavigation.vue'
import ToolbarSticky from '@/components/ToolbarSticky.vue'
import { COLLECTION_CHANGED } from '@/types/events'
import Vue from 'vue'
const cookiePageSize = 'pagesize'
@ -56,6 +57,12 @@ export default Vue.extend({
default: 0,
},
},
created () {
this.$eventHub.$on(COLLECTION_CHANGED, this.reloadCollections)
},
beforeDestroy () {
this.$eventHub.$off(COLLECTION_CHANGED, this.reloadCollections)
},
mounted () {
this.loadLibrary(this.libraryId)
},
@ -75,10 +82,16 @@ export default Vue.extend({
},
},
methods: {
reloadCollections () {
this.loadLibrary(this.libraryId)
},
async loadLibrary (libraryId: number) {
this.library = this.getLibraryLazy(libraryId)
const lib = libraryId !== 0 ? [libraryId] : undefined
this.collections = await this.$komgaCollections.getCollections(lib)
if (this.collections.length === 0) {
await this.$router.push({ name: 'browse-libraries', params: { libraryId: libraryId.toString() } })
}
},
getLibraryLazy (libraryId: any): LibraryDto | undefined {
if (libraryId !== 0) {

View file

@ -89,10 +89,6 @@
:series.sync="editSeriesSingle"
/>
<collection-add-to-dialog v-model="dialogAddToCollection"
:series="selectedSeries"
/>
<v-container fluid class="px-6">
<empty-state
v-if="totalPages === 0"
@ -124,7 +120,6 @@
<script lang="ts">
import Badge from '@/components/Badge.vue'
import CollectionAddToDialog from '@/components/CollectionAddToDialog.vue'
import EditSeriesDialog from '@/components/EditSeriesDialog.vue'
import EmptyState from '@/components/EmptyState.vue'
import FilterMenuButton from '@/components/FilterMenuButton.vue'
@ -137,6 +132,7 @@ import ToolbarSticky from '@/components/ToolbarSticky.vue'
import { parseQueryFilter, parseQuerySort } from '@/functions/query-params'
import { ReadStatus } from '@/types/enum-books'
import { SeriesStatus } from '@/types/enum-series'
import { COLLECTION_CHANGED, SERIES_CHANGED } from '@/types/events'
import Vue from 'vue'
const cookiePageSize = 'pagesize'
@ -152,7 +148,6 @@ export default Vue.extend({
Badge,
EditSeriesDialog,
ItemBrowser,
CollectionAddToDialog,
PageSizeSelect,
LibraryNavigation,
},
@ -212,6 +207,14 @@ export default Vue.extend({
}
},
},
created () {
this.$eventHub.$on(COLLECTION_CHANGED, this.reloadCollections)
this.$eventHub.$on(SERIES_CHANGED, this.reloadSeries)
},
beforeDestroy () {
this.$eventHub.$off(COLLECTION_CHANGED, this.reloadCollections)
this.$eventHub.$off(SERIES_CHANGED, this.reloadSeries)
},
mounted () {
if (this.$cookies.isKey(cookiePageSize)) {
this.pageSize = Number(this.$cookies.get(cookiePageSize))
@ -298,6 +301,14 @@ export default Vue.extend({
this.setWatches()
},
reloadCollections () {
this.loadLibrary(this.libraryId)
},
reloadSeries (event: EventSeriesChanged) {
if (this.libraryId === 0 || event.libraryId === this.libraryId) {
this.loadPage(this.libraryId, this.page, this.sortActive)
}
},
async loadLibrary (libraryId: number) {
this.library = this.getLibraryLazy(libraryId)
@ -371,7 +382,7 @@ export default Vue.extend({
))
},
addToCollection () {
this.dialogAddToCollection = true
this.$store.dispatch('dialogAddSeriesToCollection', this.selectedSeries)
},
},
})

View file

@ -11,9 +11,6 @@
<series-actions-menu v-if="series"
:series="series"
@add-to-collection="addToCollection"
@mark-read="loadSeries(seriesId)"
@mark-unread="loadSeries(seriesId)"
/>
<v-toolbar-title>
@ -94,6 +91,7 @@
:item="series"
thumbnail-only
no-link
:action-menu="false"
></item-card>
</v-col>
@ -194,16 +192,11 @@
<edit-series-dialog v-model="dialogEdit" :series.sync="series"/>
<collection-add-to-dialog v-model="dialogAddToCollection"
:series="series"
@added="loadSeries(seriesId)"
/>
</div>
</template>
<script lang="ts">
import Badge from '@/components/Badge.vue'
import CollectionAddToDialog from '@/components/CollectionAddToDialog.vue'
import EditBooksDialog from '@/components/EditBooksDialog.vue'
import EditSeriesDialog from '@/components/EditSeriesDialog.vue'
import EmptyState from '@/components/EmptyState.vue'
@ -218,6 +211,7 @@ import ToolbarSticky from '@/components/ToolbarSticky.vue'
import { parseQueryFilter, parseQuerySort } from '@/functions/query-params'
import { seriesThumbnailUrl } from '@/functions/urls'
import { ReadStatus } from '@/types/enum-books'
import { BOOK_CHANGED, SERIES_CHANGED } from '@/types/events'
import Vue from 'vue'
const cookiePageSize = 'pagesize'
@ -234,7 +228,6 @@ export default Vue.extend({
ItemBrowser,
PageSizeSelect,
SeriesActionsMenu,
CollectionAddToDialog,
HorizontalScroller,
ItemCard,
EmptyState,
@ -323,6 +316,14 @@ export default Vue.extend({
}
},
},
created () {
this.$eventHub.$on(SERIES_CHANGED, this.reloadSeries)
this.$eventHub.$on(BOOK_CHANGED, this.reloadBooks)
},
beforeDestroy () {
this.$eventHub.$off(SERIES_CHANGED, this.reloadSeries)
this.$eventHub.$on(BOOK_CHANGED, this.reloadBooks)
},
mounted () {
if (this.$cookies.isKey(cookiePageSize)) {
this.pageSize = Number(this.$cookies.get(cookiePageSize))
@ -390,6 +391,12 @@ export default Vue.extend({
this.setWatches()
},
reloadSeries (event: EventSeriesChanged) {
if (event.id === this.seriesId) this.loadSeries(this.seriesId)
},
reloadBooks (event: EventBookChanged) {
if (event.seriesId === this.seriesId) this.loadSeries(this.seriesId)
},
async loadSeries (seriesId: number) {
this.series = await this.$komgaSeries.getOneSeries(seriesId)
this.collections = await this.$komgaSeries.getCollections(seriesId)
@ -460,9 +467,6 @@ export default Vue.extend({
))
this.loadSeries(this.seriesId)
},
addToCollection () {
this.dialogAddToCollection = true
},
},
})
</script>

View file

@ -109,19 +109,21 @@
</v-navigation-drawer>
<v-content>
<dialogs/>
<router-view/>
</v-content>
</div>
</template>
<script lang="ts">
import Dialogs from '@/components/Dialogs.vue'
import LibraryActionsMenu from '@/components/LibraryActionsMenu.vue'
import SearchBox from '@/components/SearchBox.vue'
import Vue from 'vue'
export default Vue.extend({
name: 'home',
components: { LibraryActionsMenu, SearchBox },
components: { LibraryActionsMenu, SearchBox, Dialogs },
data: function () {
return {
drawerVisible: this.$vuetify.breakpoint.lgAndUp,