feat(webui): multi-select collections and read lists

This commit is contained in:
Gauthier Roebroeck 2021-06-29 15:11:24 +08:00
parent 217bc493a1
commit 19e3f18cad
9 changed files with 199 additions and 76 deletions

View file

@ -12,7 +12,7 @@
<collection-delete-dialog
v-model="deleteCollectionDialog"
:collection="deleteCollection"
:collections="deleteCollections"
/>
<read-list-add-to-dialog
@ -27,7 +27,7 @@
<read-list-delete-dialog
v-model="deleteReadListDialog"
:read-list="deleteReadList"
:read-lists="deleteReadLists"
/>
<library-edit-dialog
@ -114,8 +114,8 @@ export default Vue.extend({
this.$store.dispatch('dialogDeleteCollectionDisplay', val)
},
},
deleteCollection (): CollectionDto {
return this.$store.state.deleteCollection
deleteCollections (): CollectionDto | CollectionDto[] {
return this.$store.state.deleteCollections
},
// read lists
addToReadListDialog: {
@ -148,8 +148,8 @@ export default Vue.extend({
this.$store.dispatch('dialogDeleteReadListDisplay', val)
},
},
deleteReadList (): ReadListDto {
return this.$store.state.deleteReadList
deleteReadLists (): ReadListDto | ReadListDto[] {
return this.$store.state.deleteReadLists
},
// libraries
editLibraryDialog: {

View file

@ -25,7 +25,7 @@
<v-spacer/>
<v-btn icon @click="markRead">
<v-btn icon @click="markRead" v-if="kind === 'books' || kind === 'series'">
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-icon v-on="on">mdi-bookmark-check</v-icon>
@ -34,7 +34,7 @@
</v-tooltip>
</v-btn>
<v-btn icon @click="markUnread">
<v-btn icon @click="markUnread" v-if="kind === 'books' || kind === 'series'">
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-icon v-on="on">mdi-bookmark-remove</v-icon>
@ -61,7 +61,7 @@
</v-tooltip>
</v-btn>
<v-btn icon @click="edit" v-if="isAdmin">
<v-btn icon @click="edit" v-if="isAdmin && (kind === 'books' || kind === 'series')">
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-icon v-on="on">mdi-pencil</v-icon>
@ -69,6 +69,15 @@
<span>{{ $t('menu.edit_metadata') }}</span>
</v-tooltip>
</v-btn>
<v-btn icon @click="doDelete" v-if="isAdmin && (kind === 'collections' || kind === 'readlists')">
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-icon v-on="on">mdi-delete</v-icon>
</template>
<span>{{ $t('menu.delete') }}</span>
</v-tooltip>
</v-btn>
</toolbar-sticky>
</v-scroll-y-transition>
</template>
@ -88,7 +97,10 @@ export default Vue.extend({
type: Array,
required: true,
},
// books or series
/**
* The kind of items this toolbar acts on.
* @values books, series, collections, readlists
*/
kind: {
type: String,
required: true,
@ -125,6 +137,9 @@ export default Vue.extend({
edit () {
this.$emit('edit')
},
doDelete () {
this.$emit('delete')
},
},
})
</script>

View file

@ -4,19 +4,30 @@
max-width="450"
>
<v-card>
<v-card-title>{{ $t('dialog.delete_collection.dialog_title') }}</v-card-title>
<v-card-title v-if="single">{{ $t('dialog.delete_collection.dialog_title') }}</v-card-title>
<v-card-title v-else>{{ $t('dialog.delete_collection.dialog_title_multiple') }}</v-card-title>
<v-card-text>
<v-container fluid>
<v-row>
<v-col v-html="$t('dialog.delete_collection.warning_html', { name: collection.name})"></v-col>
<v-col
v-if="single"
v-html="$t('dialog.delete_collection.warning_html', { name: collections.name})"
/>
<v-col
v-else
v-html="$t('dialog.delete_collection.warning_multiple_html', { count: collections.length})"
/>
</v-row>
<v-row>
<v-col>
<v-checkbox v-model="confirmDelete" color="red">
<template v-slot:label>
{{ $t('dialog.delete_collection.confirm_delete', {name: collection.name}) }}
<template v-slot:label v-if="single">
{{ $t('dialog.delete_collection.confirm_delete', {name: collections.name}) }}
</template>
<template v-slot:label v-else>
{{ $t('dialog.delete_collection.confirm_delete_multiple', {count: collections.length}) }}
</template>
</v-checkbox>
</v-col>
@ -66,8 +77,8 @@ export default Vue.extend({
},
props: {
value: Boolean,
collection: {
type: Object as () => CollectionDto,
collections: {
type: [Object as () => CollectionDto, Array as () => CollectionDto[]],
required: true,
},
},
@ -79,24 +90,32 @@ export default Vue.extend({
!val && this.dialogCancel()
},
},
computed: {
single(): boolean {
return !Array.isArray(this.collections)
},
},
methods: {
dialogCancel() {
this.$emit('input', false)
this.confirmDelete = false
},
dialogConfirm() {
this.deleteCollection()
this.deleteCollections()
this.$emit('input', false)
},
showSnack(message: string) {
this.snackText = message
this.snackbar = true
},
async deleteCollection() {
try {
await this.$komgaCollections.deleteCollection(this.collection.id)
} catch (e) {
this.showSnack(e.message)
async deleteCollections() {
const toUpdate = (this.single ? [this.collections] : this.collections) as CollectionDto[]
for (const b of toUpdate) {
try {
await this.$komgaCollections.deleteCollection(b.id)
} catch (e) {
this.showSnack(e.message)
}
}
},
},

View file

@ -4,19 +4,30 @@
max-width="450"
>
<v-card>
<v-card-title>{{ $t('dialog.delete_readlist.dialog_title') }}</v-card-title>
<v-card-title v-if="single">{{ $t('dialog.delete_readlist.dialog_title') }}</v-card-title>
<v-card-title v-else>{{ $t('dialog.delete_readlist.dialog_title_multiple') }}</v-card-title>
<v-card-text>
<v-container fluid>
<v-row>
<v-col v-html="$t('dialog.delete_readlist.warning_html', {name: readList.name})"></v-col>
<v-col
v-if="single"
v-html="$t('dialog.delete_readlist.warning_html', {name: readLists.name})"
/>
<v-col
v-else
v-html="$t('dialog.delete_readlist.warning_multiple_html', {count: readLists.length})"
/>
</v-row>
<v-row>
<v-col>
<v-checkbox v-model="confirmDelete" color="red">
<template v-slot:label>
{{ $t('dialog.delete_readlist.confirm_delete', { name: readList.name}) }}
<template v-slot:label v-if="single">
{{ $t('dialog.delete_readlist.confirm_delete', { name: readLists.name}) }}
</template>
<template v-slot:label v-else>
{{ $t('dialog.delete_readlist.confirm_delete_multiple', { count: readLists.length}) }}
</template>
</v-checkbox>
</v-col>
@ -64,8 +75,8 @@ export default Vue.extend({
},
props: {
value: Boolean,
readList: {
type: Object as () => ReadListDto,
readLists: {
type: [Object as () => ReadListDto, Array as () => ReadListDto[]],
required: true,
},
},
@ -77,6 +88,11 @@ export default Vue.extend({
!val && this.dialogCancel()
},
},
computed: {
single(): boolean {
return !Array.isArray(this.readLists)
},
},
methods: {
dialogCancel() {
this.$emit('input', false)
@ -91,10 +107,13 @@ export default Vue.extend({
this.snackbar = true
},
async delete() {
try {
await this.$komgaReadLists.deleteReadList(this.readList.id)
} catch (e) {
this.showSnack(e.message)
const toUpdate = (this.single ? [this.readLists] : this.readLists) as ReadListDto[]
for (const b of toUpdate) {
try {
await this.$komgaReadLists.deleteReadList(b.id)
} catch (e) {
this.showSnack(e.message)
}
}
},
},

View file

@ -255,8 +255,11 @@
"button_cancel": "Cancel",
"button_confirm": "Delete",
"confirm_delete": "Yes, delete the collection \"{name}\"",
"confirm_delete_multiple": "Yes, delete {count} collections",
"dialog_title": "Delete Collection",
"warning_html": "The collection <b>{name}</b> will be removed from this server. Your media files will not be affected. This <b>cannot</b> be undone. Continue?"
"dialog_title_multiple": "Delete Collections",
"warning_html": "The collection <b>{name}</b> will be removed from this server. Your media files will not be affected. This <b>cannot</b> be undone. Continue?",
"warning_multiple_html": "{count} collections will be removed from this server. Your media files will not be affected. This <b>cannot</b> be undone. Continue?"
},
"delete_library": {
"button_cancel": "Cancel",
@ -269,8 +272,11 @@
"button_cancel": "Cancel",
"button_confirm": "Delete",
"confirm_delete": "Yes, delete the read list \"{name}\"",
"confirm_delete_multiple": "Yes, delete {count} read lists",
"dialog_title": "Delete Read List",
"warning_html": "The read list <b>{name}</b> will be removed from this server. Your media files will not be affected. This <b>cannot</b> be undone. Continue?"
"dialog_title_multiple": "Delete Read Lists",
"warning_html": "The read list <b>{name}</b> will be removed from this server. Your media files will not be affected. This <b>cannot</b> be undone. Continue?",
"warning_multiple_html": "{count} read lists will be removed from this server. Your media files will not be affected. This <b>cannot</b> be undone. Continue?"
},
"delete_user": {
"button_cancel": "Cancel",
@ -540,6 +546,7 @@
"add_to_readlist": "Add to read list",
"analyze": "Analyze",
"delete": "Delete",
"deselect_all": "Deselect all",
"download_series": "Download series",
"edit": "Edit",
"edit_metadata": "Edit metadata",
@ -547,7 +554,6 @@
"mark_unread": "Mark as unread",
"refresh_metadata": "Refresh metadata",
"scan_library_files": "Scan library files",
"deselect_all": "Deselect all",
"select_all": "Select all"
},
"navigation": {

View file

@ -18,14 +18,14 @@ export default new Vuex.Store({
addToCollectionDialog: false,
editCollection: {} as CollectionDto,
editCollectionDialog: false,
deleteCollection: {} as CollectionDto,
deleteCollections: {} as CollectionDto | CollectionDto[],
deleteCollectionDialog: false,
// read lists
addToReadListBooks: {} as BookDto | BookDto[],
addToReadListDialog: false,
editReadList: {} as ReadListDto,
editReadListDialog: false,
deleteReadList: {} as ReadListDto,
deleteReadLists: {} as ReadListDto | ReadListDto[],
deleteReadListDialog: false,
// libraries
editLibrary: {} as LibraryDto | undefined,
@ -53,27 +53,27 @@ export default new Vuex.Store({
setEditCollectionDialog (state, dialog) {
state.editCollectionDialog = dialog
},
setDeleteCollection (state, collection) {
state.deleteCollection = collection
setDeleteCollections (state, collections) {
state.deleteCollections = collections
},
setDeleteCollectionDialog (state, dialog) {
state.deleteCollectionDialog = dialog
},
// Read Lists
setAddToReadListBooks (state, Book) {
state.addToReadListBooks = Book
setAddToReadListBooks (state, book) {
state.addToReadListBooks = book
},
setAddToReadListDialog (state, dialog) {
state.addToReadListDialog = dialog
},
setEditReadList (state, ReadList) {
state.editReadList = ReadList
setEditReadList (state, readList) {
state.editReadList = readList
},
setEditReadListDialog (state, dialog) {
state.editReadListDialog = dialog
},
setDeleteReadList (state, ReadList) {
state.deleteReadList = ReadList
setDeleteReadLists (state, readLists) {
state.deleteReadLists = readLists
},
setDeleteReadListDialog (state, dialog) {
state.deleteReadListDialog = dialog
@ -122,8 +122,8 @@ export default new Vuex.Store({
dialogEditCollectionDisplay ({ commit }, value) {
commit('setEditCollectionDialog', value)
},
dialogDeleteCollection ({ commit }, collection) {
commit('setDeleteCollection', collection)
dialogDeleteCollection ({ commit }, collections) {
commit('setDeleteCollections', collections)
commit('setDeleteCollectionDialog', true)
},
dialogDeleteCollectionDisplay ({ commit }, value) {
@ -144,8 +144,8 @@ export default new Vuex.Store({
dialogEditReadListDisplay ({ commit }, value) {
commit('setEditReadListDialog', value)
},
dialogDeleteReadList ({ commit }, readList) {
commit('setDeleteReadList', readList)
dialogDeleteReadList ({ commit }, readLists) {
commit('setDeleteReadLists', readLists)
commit('setDeleteReadListDialog', true)
},
dialogDeleteReadListDisplay ({ commit }, value) {

View file

@ -1,6 +1,6 @@
<template>
<div :style="$vuetify.breakpoint.name === 'xs' ? 'margin-bottom: 56px' : undefined">
<toolbar-sticky>
<toolbar-sticky v-if="selectedCollections.length === 0">
<!-- Action menu -->
<library-actions-menu v-if="library"
:library="library"/>
@ -21,6 +21,15 @@
<page-size-select v-model="pageSize"/>
</toolbar-sticky>
<multi-select-bar
v-model="selectedCollections"
kind="collections"
show-select-all
@unselect-all="selectedCollections = []"
@select-all="selectedCollections = collections"
@delete="deleteCollections"
/>
<library-navigation v-if="$vuetify.breakpoint.name === 'xs'" :libraryId="libraryId" bottom-navigation/>
<v-container fluid>
@ -33,7 +42,8 @@
<item-browser
:items="collections"
:selectable="false"
selectable
:selected.sync="selectedCollections"
:edit-function="editSingleCollection"
/>
@ -59,6 +69,7 @@ import Vue from 'vue'
import {Location} from 'vue-router'
import {LIBRARIES_ALL, LIBRARY_ROUTE} from '@/types/library'
import {LibrarySseDto} from "@/types/komga-sse";
import MultiSelectBar from "@/components/bars/MultiSelectBar.vue";
export default Vue.extend({
name: 'BrowseCollections',
@ -68,11 +79,13 @@ export default Vue.extend({
LibraryNavigation,
ItemBrowser,
PageSizeSelect,
MultiSelectBar,
},
data: () => {
return {
library: undefined as LibraryDto | undefined,
collections: [] as CollectionDto[],
selectedCollections: [] as CollectionDto[],
page: 1,
pageSize: 20,
totalPages: 1,
@ -196,6 +209,8 @@ export default Vue.extend({
}
},
async loadPage(libraryId: string, page: number) {
this.selectedCollections = []
const pageRequest = {
page: page - 1,
size: this.pageSize,
@ -218,6 +233,9 @@ export default Vue.extend({
editSingleCollection(collection: CollectionDto) {
this.$store.dispatch('dialogEditCollection', collection)
},
deleteCollections() {
this.$store.dispatch('dialogDeleteCollection', this.selectedCollections)
},
},
})
</script>

View file

@ -1,6 +1,6 @@
<template>
<div :style="$vuetify.breakpoint.name === 'xs' ? 'margin-bottom: 56px' : undefined">
<toolbar-sticky>
<toolbar-sticky v-if="selectedReadLists.length === 0">
<!-- Action menu -->
<library-actions-menu v-if="library"
:library="library"/>
@ -21,6 +21,15 @@
<page-size-select v-model="pageSize"/>
</toolbar-sticky>
<multi-select-bar
v-model="selectedReadLists"
kind="readlists"
show-select-all
@unselect-all="selectedReadLists = []"
@select-all="selectedReadLists = readLists"
@delete="deleteReadLists"
/>
<library-navigation v-if="$vuetify.breakpoint.name === 'xs'" :libraryId="libraryId" bottom-navigation/>
<v-container fluid>
@ -33,7 +42,8 @@
<item-browser
:items="readLists"
:selectable="false"
selectable
:selected.sync="selectedReadLists"
:edit-function="editSingle"
/>
@ -59,6 +69,7 @@ import Vue from 'vue'
import {Location} from 'vue-router'
import {LIBRARIES_ALL, LIBRARY_ROUTE} from '@/types/library'
import {LibrarySseDto} from "@/types/komga-sse";
import MultiSelectBar from "@/components/bars/MultiSelectBar.vue";
export default Vue.extend({
name: 'BrowseReadLists',
@ -68,11 +79,13 @@ export default Vue.extend({
LibraryNavigation,
ItemBrowser,
PageSizeSelect,
MultiSelectBar,
},
data: () => {
return {
library: undefined as LibraryDto | undefined,
readLists: [] as ReadListDto[],
selectedReadLists: [] as ReadListDto[],
page: 1,
pageSize: 20,
totalPages: 1,
@ -87,19 +100,19 @@ export default Vue.extend({
default: LIBRARIES_ALL,
},
},
created () {
created() {
this.$eventHub.$on(READLIST_ADDED, this.reloadElements)
this.$eventHub.$on(READLIST_CHANGED, this.reloadElements)
this.$eventHub.$on(READLIST_DELETED, this.reloadElements)
this.$eventHub.$on(LIBRARY_CHANGED, this.reloadLibrary)
},
beforeDestroy () {
beforeDestroy() {
this.$eventHub.$off(READLIST_ADDED, this.reloadElements)
this.$eventHub.$off(READLIST_CHANGED, this.reloadElements)
this.$eventHub.$off(READLIST_DELETED, this.reloadElements)
this.$eventHub.$off(LIBRARY_CHANGED, this.reloadLibrary)
},
mounted () {
mounted() {
this.$store.commit('setLibraryRoute', {id: this.libraryId, route: LIBRARY_ROUTE.READLISTS})
this.pageSize = this.$store.state.persistedState.browsingPageSize || this.pageSize
@ -111,7 +124,7 @@ export default Vue.extend({
this.setWatches()
},
beforeRouteUpdate (to, from, next) {
beforeRouteUpdate(to, from, next) {
if (to.params.libraryId !== from.params.libraryId) {
// reset
this.page = 1
@ -125,10 +138,10 @@ export default Vue.extend({
next()
},
computed: {
isAdmin (): boolean {
isAdmin(): boolean {
return this.$store.getters.meAdmin
},
paginationVisible (): number {
paginationVisible(): number {
switch (this.$vuetify.breakpoint.name) {
case 'xs':
return 5
@ -143,7 +156,7 @@ export default Vue.extend({
},
},
methods: {
setWatches () {
setWatches() {
this.pageSizeUnwatch = this.$watch('pageSize', (val) => {
this.$store.commit('setBrowsingPageSize', val)
this.updateRouteAndReload()
@ -154,11 +167,11 @@ export default Vue.extend({
this.loadPage(this.libraryId, val)
})
},
unsetWatches () {
unsetWatches() {
this.pageUnwatch()
this.pageSizeUnwatch()
},
updateRouteAndReload () {
updateRouteAndReload() {
this.unsetWatches()
this.page = 1
@ -168,10 +181,10 @@ export default Vue.extend({
this.setWatches()
},
updateRoute () {
updateRoute() {
this.$router.replace({
name: this.$route.name,
params: { libraryId: this.$route.params.libraryId },
params: {libraryId: this.$route.params.libraryId},
query: {
page: `${this.page}`,
pageSize: `${this.pageSize}`,
@ -179,23 +192,25 @@ export default Vue.extend({
} as Location).catch((_: any) => {
})
},
reloadElements () {
reloadElements() {
this.loadLibrary(this.libraryId)
},
reloadLibrary (event: LibrarySseDto) {
reloadLibrary(event: LibrarySseDto) {
if (event.libraryId === this.libraryId) {
this.loadLibrary(this.libraryId)
}
},
async loadLibrary (libraryId: string) {
async loadLibrary(libraryId: string) {
this.library = this.getLibraryLazy(libraryId)
await this.loadPage(libraryId, this.page)
if (this.totalElements === 0) {
await this.$router.push({ name: 'browse-libraries', params: { libraryId: libraryId.toString() } })
await this.$router.push({name: 'browse-libraries', params: {libraryId: libraryId.toString()}})
}
},
async loadPage (libraryId: string, page: number) {
async loadPage(libraryId: string, page: number) {
this.selectedReadLists = []
const pageRequest = {
page: page - 1,
size: this.pageSize,
@ -208,16 +223,19 @@ export default Vue.extend({
this.totalElements = elementsPage.totalElements
this.readLists = elementsPage.content
},
getLibraryLazy (libraryId: string): LibraryDto | undefined {
getLibraryLazy(libraryId: string): LibraryDto | undefined {
if (libraryId !== LIBRARIES_ALL) {
return this.$store.getters.getLibraryById(libraryId)
} else {
return undefined
}
},
editSingle (element: ReadListDto) {
editSingle(element: ReadListDto) {
this.$store.dispatch('dialogEditReadList', element)
},
deleteReadLists () {
this.$store.dispatch('dialogDeleteReadList', this.selectedReadLists)
},
},
})
</script>

View file

@ -26,6 +26,20 @@
@edit="editMultipleBooks"
/>
<multi-select-bar
v-model="selectedCollections"
kind="collections"
@unselect-all="selectedCollections = []"
@delete="deleteCollections"
/>
<multi-select-bar
v-model="selectedReadLists"
kind="readlists"
@unselect-all="selectedReadLists = []"
@delete="deleteReadLists"
/>
<v-container fluid>
<empty-state
v-if="emptyResults"
@ -47,7 +61,7 @@
nowrap
:edit-function="singleEditSeries"
:selected.sync="selectedSeries"
:selectable="selectedBooks.length === 0"
:selectable="selectedBooks.length === 0 && selectedCollections.length === 0 && selectedReadLists.length === 0"
:fixed-item-width="fixedCardWidth"
/>
</template>
@ -62,7 +76,7 @@
nowrap
:edit-function="singleEditBook"
:selected.sync="selectedBooks"
:selectable="selectedSeries.length === 0"
:selectable="selectedSeries.length === 0 && selectedCollections.length === 0 && selectedReadLists.length === 0"
:fixed-item-width="fixedCardWidth"
/>
</template>
@ -76,7 +90,8 @@
<item-browser :items="collections"
nowrap
:edit-function="singleEditCollection"
:selectable="false"
:selected.sync="selectedCollections"
:selectable="selectedSeries.length === 0 && selectedBooks.length === 0 && selectedReadLists.length === 0"
:fixed-item-width="fixedCardWidth"
/>
</template>
@ -90,7 +105,8 @@
<item-browser :items="readLists"
nowrap
:edit-function="singleEditReadList"
:selectable="false"
:selected.sync="selectedReadLists"
:selectable="selectedSeries.length === 0 && selectedBooks.length === 0 && selectedCollections.length === 0"
:fixed-item-width="fixedCardWidth"
/>
</template>
@ -145,6 +161,8 @@ export default Vue.extend({
loading: false,
selectedSeries: [] as SeriesDto[],
selectedBooks: [] as BookDto[],
selectedCollections: [] as CollectionDto[],
selectedReadLists: [] as ReadListDto[],
}
},
created () {
@ -179,6 +197,8 @@ export default Vue.extend({
this.loadResults(val)
this.selectedBooks = []
this.selectedSeries = []
this.selectedCollections = []
this.selectedReadLists = []
},
deep: true,
immediate: true,
@ -189,7 +209,7 @@ export default Vue.extend({
return this.$vuetify.breakpoint.name === 'xs' ? 120 : 150
},
showToolbar (): boolean {
return this.selectedSeries.length === 0 && this.selectedBooks.length === 0
return this.selectedSeries.length === 0 && this.selectedBooks.length === 0 && this.selectedCollections.length === 0 && this.selectedReadLists.length === 0
},
emptyResults (): boolean {
return !this.loading && this.series.length === 0 && this.books.length === 0 && this.collections.length === 0 && this.readLists.length === 0
@ -261,6 +281,12 @@ export default Vue.extend({
editMultipleBooks () {
this.$store.dispatch('dialogUpdateBooks', this.selectedBooks)
},
deleteCollections () {
this.$store.dispatch('dialogDeleteCollection', this.selectedCollections)
},
deleteReadLists () {
this.$store.dispatch('dialogDeleteReadList', this.selectedReadLists)
},
async markSelectedBooksRead () {
await Promise.all(this.selectedBooks.map(b =>
this.$komgaBooks.updateReadProgress(b.id, { completed: true }),
@ -277,6 +303,8 @@ export default Vue.extend({
async loadResults (search: string) {
this.selectedBooks = []
this.selectedSeries = []
this.selectedCollections = []
this.selectedReadLists = []
if (search) {
this.loading = true