mirror of
https://github.com/gotson/komga.git
synced 2025-12-22 00:13:30 +01:00
parent
c2f940336a
commit
2f8255a05f
24 changed files with 1440 additions and 82 deletions
22
komga-webui/package-lock.json
generated
22
komga-webui/package-lock.json
generated
|
|
@ -2898,6 +2898,15 @@
|
|||
"integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/vuedraggable": {
|
||||
"version": "2.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/vuedraggable/-/vuedraggable-2.23.1.tgz",
|
||||
"integrity": "sha512-icDSUwIc1xqtMBxYlaO76ywpzxBumcGV3gC7WVtRl37A+TNuNFg3bMwPpi3M5KZWrIYChBmM9uwMNB7C8hvWWQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"vue": ">=2.0.0"
|
||||
}
|
||||
},
|
||||
"@types/vuelidate": {
|
||||
"version": "0.7.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/vuelidate/-/vuelidate-0.7.10.tgz",
|
||||
|
|
@ -15505,6 +15514,11 @@
|
|||
"is-plain-obj": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"sortablejs": {
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.10.2.tgz",
|
||||
"integrity": "sha512-YkPGufevysvfwn5rfdlGyrGjt7/CRHwvRPogD/lC+TnvcN29jDpCifKP+rBqf+LRldfXSTh+0CGLcSg0VIxq3A=="
|
||||
},
|
||||
"source-list-map": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
|
||||
|
|
@ -16945,6 +16959,14 @@
|
|||
"resolved": "https://registry.npmjs.org/vue-typed-mixins/-/vue-typed-mixins-0.2.0.tgz",
|
||||
"integrity": "sha512-0OxuinandPWv3nm5k/reYkuKtX3jjPZ40Sy9roJz0ih8PUzmI7zSRiXFEJ62LsyRegw9Tqy+qMkajk7ipKP8Vg=="
|
||||
},
|
||||
"vuedraggable": {
|
||||
"version": "2.23.2",
|
||||
"resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-2.23.2.tgz",
|
||||
"integrity": "sha512-PgHCjUpxEAEZJq36ys49HfQmXglattf/7ofOzUrW2/rRdG7tu6fK84ir14t1jYv4kdXewTEa2ieKEAhhEMdwkQ==",
|
||||
"requires": {
|
||||
"sortablejs": "^1.10.1"
|
||||
}
|
||||
},
|
||||
"vuelidate": {
|
||||
"version": "0.7.5",
|
||||
"resolved": "https://registry.npmjs.org/vuelidate/-/vuelidate-0.7.5.tgz",
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
"vue-moment": "^4.1.0",
|
||||
"vue-router": "^3.1.5",
|
||||
"vue-typed-mixins": "^0.2.0",
|
||||
"vuedraggable": "^2.23.2",
|
||||
"vuelidate": "^0.7.5",
|
||||
"vuetify": "^2.2.14",
|
||||
"vuex": "^3.1.2",
|
||||
|
|
@ -31,6 +32,7 @@
|
|||
"@mdi/font": "^4.9.95",
|
||||
"@types/jest": "^25.1.3",
|
||||
"@types/lodash": "^4.14.149",
|
||||
"@types/vuedraggable": "^2.23.1",
|
||||
"@types/vuelidate": "^0.7.10",
|
||||
"@vue/cli-plugin-babel": "^4.2.2",
|
||||
"@vue/cli-plugin-eslint": "^4.2.2",
|
||||
|
|
|
|||
64
komga-webui/src/components/CollectionActionsMenu.vue
Normal file
64
komga-webui/src/components/CollectionActionsMenu.vue
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-menu offset-y v-if="isAdmin">
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn icon v-on="on" @click.prevent="">
|
||||
<v-icon>mdi-dots-vertical</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item @click="promptDeleteCollection"
|
||||
class="list-warning">
|
||||
<v-list-item-title>Delete</v-list-item-title>
|
||||
</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,
|
||||
}
|
||||
},
|
||||
props: {
|
||||
collection: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isAdmin (): boolean {
|
||||
return this.$store.getters.meAdmin
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
promptDeleteCollection () {
|
||||
this.modalDeleteCollection = true
|
||||
},
|
||||
afterDelete () {
|
||||
this.$emit('deleted', true)
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
<style scoped>
|
||||
.list-warning:hover {
|
||||
background: #F44336;
|
||||
}
|
||||
|
||||
.list-warning:hover .v-list-item__title {
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
174
komga-webui/src/components/CollectionAddToDialog.vue
Normal file
174
komga-webui/src/components/CollectionAddToDialog.vue
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-dialog v-model="modal"
|
||||
max-width="450"
|
||||
scrollable
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>Add to collection</v-card-title>
|
||||
<v-btn icon absolute top right @click="dialogClose">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-divider/>
|
||||
|
||||
<v-card-text style="height: 50%">
|
||||
<v-container fluid>
|
||||
|
||||
<v-row align="center">
|
||||
<v-col>
|
||||
<v-text-field
|
||||
v-model="newCollection"
|
||||
label="Create new collection"
|
||||
@keydown.enter="create"
|
||||
:error-messages="duplicate"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="auto">
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="create"
|
||||
:disabled="newCollection.length === 0 || duplicate !== ''"
|
||||
>Create
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-divider/>
|
||||
|
||||
<v-row v-if="collections.length !== 0">
|
||||
<v-col>
|
||||
<v-list elevation="5">
|
||||
<div v-for="(c, index) in collections"
|
||||
:key="index"
|
||||
>
|
||||
<v-list-item @click="addTo(c)"
|
||||
two-line
|
||||
>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ c.name }}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ c.seriesIds.length }} series</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-divider v-if="index !== collections.length-1"/>
|
||||
</div>
|
||||
</v-list>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-snackbar
|
||||
v-model="snackbar"
|
||||
bottom
|
||||
color="error"
|
||||
>
|
||||
{{ snackText }}
|
||||
<v-btn
|
||||
text
|
||||
@click="snackbar = false"
|
||||
>
|
||||
Close
|
||||
</v-btn>
|
||||
</v-snackbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'CollectionAddToDialog',
|
||||
data: () => {
|
||||
return {
|
||||
confirmDelete: false,
|
||||
snackbar: false,
|
||||
snackText: '',
|
||||
modal: false,
|
||||
collections: [] as CollectionDto[],
|
||||
selectedCollection: null,
|
||||
newCollection: '',
|
||||
}
|
||||
},
|
||||
props: {
|
||||
value: Boolean,
|
||||
series: {
|
||||
type: [Object as () => SeriesDto, Array as () => SeriesDto[]],
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
async value (val) {
|
||||
this.modal = val
|
||||
if (val) {
|
||||
this.newCollection = ''
|
||||
this.collections = await this.$komgaCollections.getCollections()
|
||||
}
|
||||
},
|
||||
modal (val) {
|
||||
!val && this.dialogClose()
|
||||
},
|
||||
},
|
||||
async mounted () {
|
||||
|
||||
},
|
||||
computed: {
|
||||
seriesIds (): number[] {
|
||||
if (Array.isArray(this.series)) return this.series.map(s => s.id)
|
||||
else return [this.series.id]
|
||||
},
|
||||
duplicate (): string {
|
||||
if (this.newCollection !== '' && this.collections.some(e => e.name === this.newCollection)) {
|
||||
return 'A collection with this name already exists'
|
||||
} else return ''
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
dialogClose () {
|
||||
this.$emit('input', false)
|
||||
},
|
||||
showSnack (message: string) {
|
||||
this.snackText = message
|
||||
this.snackbar = true
|
||||
},
|
||||
async addTo (collection: CollectionDto) {
|
||||
const seriesIds = this.$_.uniq(collection.seriesIds.concat(this.seriesIds))
|
||||
|
||||
const toUpdate = {
|
||||
seriesIds: seriesIds,
|
||||
} as CollectionUpdateDto
|
||||
|
||||
try {
|
||||
await this.$komgaCollections.patchCollection(collection.id, toUpdate)
|
||||
this.$emit('added', true)
|
||||
this.dialogClose()
|
||||
} catch (e) {
|
||||
this.showSnack(e.message)
|
||||
}
|
||||
},
|
||||
async create () {
|
||||
const toCreate = {
|
||||
name: this.newCollection,
|
||||
ordered: false,
|
||||
seriesIds: this.seriesIds,
|
||||
} as CollectionCreationDto
|
||||
|
||||
try {
|
||||
await this.$komgaCollections.postCollection(toCreate)
|
||||
this.$emit('added', true)
|
||||
this.dialogClose()
|
||||
} catch (e) {
|
||||
this.showSnack(e.message)
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
114
komga-webui/src/components/CollectionDeleteDialog.vue
Normal file
114
komga-webui/src/components/CollectionDeleteDialog.vue
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-dialog v-model="modal"
|
||||
max-width="450"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>Delete Collection</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<v-container fluid>
|
||||
<v-row>
|
||||
<v-col>The collection <b>{{ collection.name }}</b> will be removed from this server. Your media files will
|
||||
not
|
||||
be
|
||||
affected. This <b>cannot</b> be undone. Continue ?
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-checkbox v-model="confirmDelete" color="red">
|
||||
<template v-slot:label>
|
||||
Yes, delete the collection "{{ collection.name }}"
|
||||
</template>
|
||||
</v-checkbox>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer/>
|
||||
<v-btn text @click="dialogCancel">Cancel</v-btn>
|
||||
<v-btn text class="red--text"
|
||||
@click="dialogConfirm"
|
||||
:disabled="!confirmDelete"
|
||||
>Delete
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-snackbar
|
||||
v-model="snackbar"
|
||||
bottom
|
||||
color="error"
|
||||
>
|
||||
{{ snackText }}
|
||||
<v-btn
|
||||
text
|
||||
@click="snackbar = false"
|
||||
>
|
||||
Close
|
||||
</v-btn>
|
||||
</v-snackbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'CollectionDeleteDialog',
|
||||
data: () => {
|
||||
return {
|
||||
confirmDelete: false,
|
||||
snackbar: false,
|
||||
snackText: '',
|
||||
modal: false,
|
||||
}
|
||||
},
|
||||
props: {
|
||||
value: Boolean,
|
||||
collection: {
|
||||
type: Object as () => CollectionDto,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
value (val) {
|
||||
this.modal = val
|
||||
},
|
||||
modal (val) {
|
||||
!val && this.dialogCancel()
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
dialogCancel () {
|
||||
this.$emit('input', false)
|
||||
this.confirmDelete = false
|
||||
},
|
||||
dialogConfirm () {
|
||||
this.deleteCollection()
|
||||
this.$emit('input', false)
|
||||
},
|
||||
showSnack (message: string) {
|
||||
this.snackText = message
|
||||
this.snackbar = true
|
||||
},
|
||||
async deleteCollection () {
|
||||
try {
|
||||
await this.$komgaCollections.deleteCollection(this.collection.id)
|
||||
this.$emit('deleted', true)
|
||||
} catch (e) {
|
||||
this.showSnack(e.message)
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
154
komga-webui/src/components/CollectionEditDialog.vue
Normal file
154
komga-webui/src/components/CollectionEditDialog.vue
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-dialog v-model="modal"
|
||||
max-width="450"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>Edit collection</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<v-container fluid>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-text-field v-model="form.name"
|
||||
label="Name"
|
||||
:error-messages="getErrorsName"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col>
|
||||
<div class="body-2">By default, series in a collection will be ordered by name. You can enable manual
|
||||
ordering to define your own order.
|
||||
</div>
|
||||
<v-checkbox
|
||||
v-model="form.ordered"
|
||||
label="Manual ordering"
|
||||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer/>
|
||||
<v-btn text @click="dialogCancel">Cancel</v-btn>
|
||||
<v-btn text class="primary--text"
|
||||
@click="dialogConfirm"
|
||||
:disabled="getErrorsName !== ''"
|
||||
>Save changes
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-snackbar
|
||||
v-model="snackbar"
|
||||
bottom
|
||||
color="error"
|
||||
>
|
||||
{{ snackText }}
|
||||
<v-btn
|
||||
text
|
||||
@click="snackbar = false"
|
||||
>
|
||||
Close
|
||||
</v-btn>
|
||||
</v-snackbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { UserRoles } from '@/types/enum-users'
|
||||
import Vue from 'vue'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'CollectionEditDialog',
|
||||
data: () => {
|
||||
return {
|
||||
UserRoles,
|
||||
snackbar: false,
|
||||
snackText: '',
|
||||
modal: false,
|
||||
collections: [] as CollectionDto[],
|
||||
form: {
|
||||
name: '',
|
||||
ordered: false,
|
||||
},
|
||||
}
|
||||
},
|
||||
props: {
|
||||
value: Boolean,
|
||||
collection: {
|
||||
type: Object as () => CollectionDto,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
value (val) {
|
||||
this.modal = val
|
||||
},
|
||||
modal (val) {
|
||||
!val && this.dialogCancel()
|
||||
},
|
||||
collection: {
|
||||
handler (val) {
|
||||
this.dialogReset(val)
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
libraries (): LibraryDto[] {
|
||||
return this.$store.state.komgaLibraries.libraries
|
||||
},
|
||||
getErrorsName (): string {
|
||||
if (this.form.name === '') return 'Name is required'
|
||||
if (this.form.name !== this.collection.name && this.collections.some(e => e.name === this.form.name)) {
|
||||
return 'A collection with this name already exists'
|
||||
}
|
||||
return ''
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async dialogReset (collection: CollectionDto) {
|
||||
this.form.name = collection.name
|
||||
this.form.ordered = collection.ordered
|
||||
this.collections = await this.$komgaCollections.getCollections()
|
||||
},
|
||||
dialogCancel () {
|
||||
this.$emit('input', false)
|
||||
this.dialogReset(this.collection)
|
||||
},
|
||||
dialogConfirm () {
|
||||
this.editCollection()
|
||||
this.$emit('input', false)
|
||||
this.dialogReset(this.collection)
|
||||
},
|
||||
showSnack (message: string) {
|
||||
this.snackText = message
|
||||
this.snackbar = true
|
||||
},
|
||||
async editCollection () {
|
||||
try {
|
||||
const update = {
|
||||
name: this.form.name,
|
||||
ordered: this.form.ordered,
|
||||
} as CollectionUpdateDto
|
||||
|
||||
await this.$komgaCollections.patchCollection(this.collection.id, update)
|
||||
this.$emit('updated', true)
|
||||
} catch (e) {
|
||||
this.showSnack(e.message)
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
<template>
|
||||
<div style="position: relative">
|
||||
<slot name="prepend"/>
|
||||
<div style="min-height: 36px">
|
||||
<slot name="prepend"/>
|
||||
</div>
|
||||
<div style="position: absolute; top: 0; right: 0">
|
||||
<v-btn icon
|
||||
:disabled="!canScrollLeft"
|
||||
|
|
|
|||
|
|
@ -1,25 +1,65 @@
|
|||
<template>
|
||||
<v-item-group multiple v-model="selectedItems">
|
||||
<v-row justify="start" ref="content" v-resize="onResize" v-if="hasItems">
|
||||
<v-item
|
||||
v-for="(item, index) in items"
|
||||
:key="index"
|
||||
class="my-3 mx-2"
|
||||
v-slot:default="{ toggle, active }" :value="$_.get(item, 'id', 0)"
|
||||
<div v-if="hasItems"
|
||||
ref="content"
|
||||
v-resize="onResize"
|
||||
>
|
||||
<draggable v-model="localItems"
|
||||
class="d-flex flex-wrap"
|
||||
v-bind="dragOptions"
|
||||
>
|
||||
<slot name="item"
|
||||
v-bind:data="{ toggle, active, item, index, itemWidth, preselect: shouldPreselect(), editItem }">
|
||||
<item-card
|
||||
:item="item"
|
||||
:width="itemWidth"
|
||||
:selected="active"
|
||||
:preselect="shouldPreselect()"
|
||||
:onEdit="editItem"
|
||||
:onSelected="toggle"
|
||||
></item-card>
|
||||
</slot>
|
||||
</v-item>
|
||||
</v-row>
|
||||
<transition-group type="transition" :name="!draggable ? 'flip-list' : null"
|
||||
class="d-flex flex-wrap"
|
||||
>
|
||||
<v-item
|
||||
v-for="item in localItems"
|
||||
:key="item.id"
|
||||
class="my-3 mx-2"
|
||||
v-slot:default="{ toggle, active }" :value="$_.get(item, 'id', 0)"
|
||||
>
|
||||
<slot name="item">
|
||||
<div style="position: relative"
|
||||
:class="draggable ? 'draggable-item' : undefined"
|
||||
>
|
||||
<item-card
|
||||
:item="item"
|
||||
:width="itemWidth"
|
||||
:selected="active"
|
||||
:no-link="draggable || deletable"
|
||||
:preselect="shouldPreselect"
|
||||
:onEdit="(draggable || deletable) ? undefined : editFunction"
|
||||
:onSelected="(draggable || deletable) ? undefined : selectable ? toggle: undefined"
|
||||
></item-card>
|
||||
|
||||
<v-slide-y-reverse-transition>
|
||||
<v-icon v-if="draggable"
|
||||
style="position: absolute; bottom: 0; left: 50%; margin-left: -12px;"
|
||||
>
|
||||
mdi-drag-horizontal
|
||||
</v-icon>
|
||||
</v-slide-y-reverse-transition>
|
||||
|
||||
<!-- FAB delete (center) -->
|
||||
<v-fab-transition>
|
||||
<v-btn
|
||||
v-if="deletable"
|
||||
fab
|
||||
small
|
||||
color="accent"
|
||||
class="fab-delete"
|
||||
@click="deleteItem(item)"
|
||||
style="position: absolute; bottom: 10px; right: 10px;"
|
||||
>
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
</v-fab-transition>
|
||||
|
||||
</div>
|
||||
</slot>
|
||||
</v-item>
|
||||
</transition-group>
|
||||
</draggable>
|
||||
</div>
|
||||
<v-row v-else justify="center">
|
||||
<slot name="empty"></slot>
|
||||
</v-row>
|
||||
|
|
@ -30,18 +70,23 @@
|
|||
import ItemCard from '@/components/ItemCard.vue'
|
||||
import { computeCardWidth } from '@/functions/grid-utilities'
|
||||
import Vue from 'vue'
|
||||
import draggable from 'vuedraggable'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'ItemBrowser',
|
||||
components: { ItemCard },
|
||||
components: { ItemCard, draggable },
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
selectable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
selected: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => [],
|
||||
},
|
||||
editFunction: {
|
||||
type: Function,
|
||||
|
|
@ -49,10 +94,19 @@ export default Vue.extend({
|
|||
resizeFunction: {
|
||||
type: Function,
|
||||
},
|
||||
draggable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
deletable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
selectedItems: [],
|
||||
localItems: [],
|
||||
width: 150,
|
||||
}
|
||||
},
|
||||
|
|
@ -69,6 +123,18 @@ export default Vue.extend({
|
|||
},
|
||||
immediate: true,
|
||||
},
|
||||
items: {
|
||||
handler () {
|
||||
this.localItems = this.items as []
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
localItems: {
|
||||
handler () {
|
||||
this.$emit('update:items', this.localItems)
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
hasItems (): boolean {
|
||||
|
|
@ -77,25 +143,46 @@ export default Vue.extend({
|
|||
itemWidth (): number {
|
||||
return this.width
|
||||
},
|
||||
itemHeight (): number {
|
||||
return this.width / 0.7071 + 116
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
shouldPreselect (): boolean {
|
||||
return this.selectedItems.length > 0
|
||||
},
|
||||
editItem (item: any) {
|
||||
this.editFunction(item)
|
||||
dragOptions (): any {
|
||||
return {
|
||||
animation: 200,
|
||||
group: 'item-cards',
|
||||
disabled: !this.draggable,
|
||||
ghostClass: 'ghost',
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onResize () {
|
||||
const content = this.$refs.content as HTMLElement
|
||||
this.width = computeCardWidth(content.clientWidth, this.$vuetify.breakpoint.name)
|
||||
},
|
||||
deleteItem (item: any) {
|
||||
const index = this.localItems.findIndex((e: any) => e.id === item.id)
|
||||
this.localItems.splice(index, 1)
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ghost * {
|
||||
opacity: 0.5;
|
||||
background: #c8ebfb;
|
||||
}
|
||||
|
||||
.draggable-item * {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.flip-list-move {
|
||||
transition: transform 0.4s;
|
||||
}
|
||||
|
||||
.fab-delete * {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -92,14 +92,14 @@
|
|||
<script lang="ts">
|
||||
import { getReadProgress, getReadProgressPercentage } from '@/functions/book-progress'
|
||||
import { ReadStatus } from '@/types/enum-books'
|
||||
import { createItem, Item } from '@/types/items'
|
||||
import { createItem, Item, ItemTypes } from '@/types/items'
|
||||
import Vue from 'vue'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'ItemCard',
|
||||
props: {
|
||||
item: {
|
||||
type: Object as () => BookDto | SeriesDto,
|
||||
type: Object as () => BookDto | SeriesDto | CollectionDto,
|
||||
required: true,
|
||||
},
|
||||
// hide the bottom part of the card
|
||||
|
|
@ -145,12 +145,12 @@ export default Vue.extend({
|
|||
},
|
||||
computed: {
|
||||
canReadPages (): boolean {
|
||||
return this.$store.getters.mePageStreaming
|
||||
return this.$store.getters.mePageStreaming && this.computedItem.type() === ItemTypes.BOOK
|
||||
},
|
||||
overlay (): boolean {
|
||||
return this.onEdit !== undefined || this.onSelected !== undefined || this.bookReady || this.canReadPages
|
||||
},
|
||||
computedItem (): Item<BookDto | SeriesDto> {
|
||||
computedItem (): Item<BookDto | SeriesDto | CollectionDto> {
|
||||
return createItem(this.item)
|
||||
},
|
||||
disableHover (): boolean {
|
||||
|
|
@ -169,24 +169,24 @@ export default Vue.extend({
|
|||
return this.computedItem.body()
|
||||
},
|
||||
isInProgress (): boolean {
|
||||
if ('seriesId' in this.item) return getReadProgress(this.item) === ReadStatus.IN_PROGRESS
|
||||
if (this.computedItem.type() === ItemTypes.BOOK) return getReadProgress(this.item as BookDto) === ReadStatus.IN_PROGRESS
|
||||
return false
|
||||
},
|
||||
isUnread (): boolean {
|
||||
if ('seriesId' in this.item) return getReadProgress(this.item) === ReadStatus.UNREAD
|
||||
if (this.computedItem.type() === ItemTypes.BOOK) return getReadProgress(this.item as BookDto) === ReadStatus.UNREAD
|
||||
return false
|
||||
},
|
||||
unreadCount (): number | undefined {
|
||||
if (!('seriesId' in this.item)) return this.item.booksUnreadCount
|
||||
if (this.computedItem.type() === ItemTypes.SERIES) return (this.item as SeriesDto).booksUnreadCount
|
||||
return undefined
|
||||
},
|
||||
readProgressPercentage (): number {
|
||||
if ('seriesId' in this.item) return getReadProgressPercentage(this.item)
|
||||
if (this.computedItem.type() === ItemTypes.BOOK) return getReadProgressPercentage(this.item as BookDto)
|
||||
return 0
|
||||
},
|
||||
bookReady (): boolean {
|
||||
if ('seriesId' in this.item) {
|
||||
return this.item.media.status === 'READY'
|
||||
if (this.computedItem.type() === ItemTypes.BOOK) {
|
||||
return (this.item as BookDto).media.status === 'READY'
|
||||
}
|
||||
return false
|
||||
},
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ import LibraryDeleteDialog from '@/components/LibraryDeleteDialog.vue'
|
|||
import Vue from 'vue'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'library-actions-menu',
|
||||
name: 'LibraryActionsMenu',
|
||||
components: { LibraryDeleteDialog },
|
||||
data: function () {
|
||||
return {
|
||||
|
|
|
|||
34
komga-webui/src/components/LibraryNavigation.vue
Normal file
34
komga-webui/src/components/LibraryNavigation.vue
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<template>
|
||||
<v-bottom-navigation grow color="primary"
|
||||
:fixed="$vuetify.breakpoint.name === 'xs'"
|
||||
>
|
||||
<v-btn :to="{name: 'browse-libraries', params: {libraryId: libraryId}}" exact>
|
||||
<span>Browse</span>
|
||||
<v-icon>mdi-bookshelf</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn :to="{name: 'browse-collections', params: {libraryId: libraryId}}">
|
||||
<span>Collections</span>
|
||||
<v-icon>mdi-layers-triple</v-icon>
|
||||
</v-btn>
|
||||
|
||||
</v-bottom-navigation>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'LibraryNavigation',
|
||||
props: {
|
||||
libraryId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
71
komga-webui/src/components/SeriesActionsMenu.vue
Normal file
71
komga-webui/src/components/SeriesActionsMenu.vue
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-menu offset-y>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn icon v-on="on" @click.prevent="">
|
||||
<v-icon>mdi-dots-vertical</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item @click="analyze" v-if="isAdmin">
|
||||
<v-list-item-title>Analyze</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="refreshMetadata" v-if="isAdmin">
|
||||
<v-list-item-title>Refresh metadata</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="addToCollection" v-if="isAdmin">
|
||||
<v-list-item-title>Add to collection</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="markRead" v-if="!isRead">
|
||||
<v-list-item-title>Mark as read</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="markUnread" v-if="!isUnread">
|
||||
<v-list-item-title>Mark as unread</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'SeriesActionsMenu',
|
||||
props: {
|
||||
series: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isAdmin (): boolean {
|
||||
return this.$store.getters.meAdmin
|
||||
},
|
||||
isRead (): boolean {
|
||||
return this.series.booksReadCount === this.series.booksCount
|
||||
},
|
||||
isUnread (): boolean {
|
||||
return this.series.booksUnreadCount === this.series.booksCount
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
analyze () {
|
||||
this.$komgaSeries.analyzeSeries(this.series)
|
||||
},
|
||||
refreshMetadata () {
|
||||
this.$komgaSeries.refreshMetadata(this.series)
|
||||
},
|
||||
addToCollection () {
|
||||
this.$emit('add-to-collection', true)
|
||||
},
|
||||
async markRead () {
|
||||
await this.$komgaSeries.markAsRead(this.series.id)
|
||||
this.$emit('mark-read', true)
|
||||
},
|
||||
async markUnread () {
|
||||
await this.$komgaSeries.markAsUnread(this.series.id)
|
||||
this.$emit('mark-unread', true)
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
|
@ -35,3 +35,7 @@ export function bookPageThumbnailUrl (bookId: number, page: number): string {
|
|||
export function seriesThumbnailUrl (seriesId: number): string {
|
||||
return `${urls.originNoSlash}/api/v1/series/${seriesId}/thumbnail`
|
||||
}
|
||||
|
||||
export function collectionThumbnailUrl (collectionId: number): string {
|
||||
return `${urls.originNoSlash}/api/v1/collections/${collectionId}/thumbnail`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import './public-path'
|
||||
import _, { LoDashStatic } from 'lodash'
|
||||
import Vue from 'vue'
|
||||
import VueCookies from 'vue-cookies'
|
||||
|
|
@ -10,12 +9,14 @@ import App from './App.vue'
|
|||
import actuator from './plugins/actuator.plugin'
|
||||
import httpPlugin from './plugins/http.plugin'
|
||||
import komgaBooks from './plugins/komga-books.plugin'
|
||||
import komgaReferential from './plugins/komga-referential.plugin'
|
||||
import komgaCollections from './plugins/komga-collections.plugin'
|
||||
import komgaFileSystem from './plugins/komga-filesystem.plugin'
|
||||
import komgaLibraries from './plugins/komga-libraries.plugin'
|
||||
import komgaReferential from './plugins/komga-referential.plugin'
|
||||
import komgaSeries from './plugins/komga-series.plugin'
|
||||
import komgaUsers from './plugins/komga-users.plugin'
|
||||
import vuetify from './plugins/vuetify'
|
||||
import './public-path'
|
||||
import router from './router'
|
||||
import store from './store'
|
||||
|
||||
|
|
@ -27,6 +28,7 @@ Vue.use(require('vue-moment'))
|
|||
Vue.use(httpPlugin)
|
||||
Vue.use(komgaFileSystem, { http: Vue.prototype.$http })
|
||||
Vue.use(komgaSeries, { http: Vue.prototype.$http })
|
||||
Vue.use(komgaCollections, { http: Vue.prototype.$http })
|
||||
Vue.use(komgaBooks, { http: Vue.prototype.$http })
|
||||
Vue.use(komgaReferential, { http: Vue.prototype.$http })
|
||||
Vue.use(komgaUsers, { store: store, http: Vue.prototype.$http })
|
||||
|
|
|
|||
17
komga-webui/src/plugins/komga-collections.plugin.ts
Normal file
17
komga-webui/src/plugins/komga-collections.plugin.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import KomgaCollectionsService from '@/services/komga-collections.service'
|
||||
import { AxiosInstance } from 'axios'
|
||||
import _Vue from 'vue'
|
||||
|
||||
export default {
|
||||
install (
|
||||
Vue: typeof _Vue,
|
||||
{ http }: { http: AxiosInstance }) {
|
||||
Vue.prototype.$komgaCollections = new KomgaCollectionsService(http)
|
||||
},
|
||||
}
|
||||
|
||||
declare module 'vue/types/vue' {
|
||||
interface Vue {
|
||||
$komgaCollections: KomgaCollectionsService;
|
||||
}
|
||||
}
|
||||
|
|
@ -84,6 +84,19 @@ const router = new Router({
|
|||
component: () => import(/* webpackChunkName: "browse-libraries" */ './views/BrowseLibraries.vue'),
|
||||
props: (route) => ({ libraryId: Number(route.params.libraryId) }),
|
||||
},
|
||||
{
|
||||
path: '/libraries/:libraryId/collections',
|
||||
name: 'browse-collections',
|
||||
beforeEnter: noLibraryGuard,
|
||||
component: () => import(/* webpackChunkName: "browse-collections" */ './views/BrowseCollections.vue'),
|
||||
props: (route) => ({ libraryId: Number(route.params.libraryId) }),
|
||||
},
|
||||
{
|
||||
path: '/collections/:collectionId',
|
||||
name: 'browse-collection',
|
||||
component: () => import(/* webpackChunkName: "browse-collection" */ './views/BrowseCollection.vue'),
|
||||
props: (route) => ({ collectionId: Number(route.params.collectionId) }),
|
||||
},
|
||||
{
|
||||
path: '/series/:seriesId',
|
||||
name: 'browse-series',
|
||||
|
|
|
|||
92
komga-webui/src/services/komga-collections.service.ts
Normal file
92
komga-webui/src/services/komga-collections.service.ts
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import { AxiosInstance } from 'axios'
|
||||
|
||||
const qs = require('qs')
|
||||
|
||||
const API_COLLECTIONS = '/api/v1/collections'
|
||||
|
||||
export default class KomgaCollectionsService {
|
||||
private http: AxiosInstance
|
||||
|
||||
constructor (http: AxiosInstance) {
|
||||
this.http = http
|
||||
}
|
||||
|
||||
async getCollections (libraryIds?: number[]): Promise<CollectionDto[]> {
|
||||
try {
|
||||
const params = {} as any
|
||||
if (libraryIds) {
|
||||
params.library_id = libraryIds
|
||||
}
|
||||
return (await this.http.get(API_COLLECTIONS, {
|
||||
params: params,
|
||||
paramsSerializer: params => qs.stringify(params, { indices: false }),
|
||||
})).data
|
||||
} catch (e) {
|
||||
let msg = 'An error occurred while trying to retrieve collections'
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
async getOneCollection (collectionId: number): Promise<CollectionDto> {
|
||||
try {
|
||||
return (await this.http.get(`${API_COLLECTIONS}/${collectionId}`)).data
|
||||
} catch (e) {
|
||||
let msg = 'An error occurred while trying to retrieve collection'
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
async postCollection (collection: CollectionCreationDto): Promise<CollectionDto> {
|
||||
try {
|
||||
return (await this.http.post(API_COLLECTIONS, collection)).data
|
||||
} catch (e) {
|
||||
let msg = `An error occurred while trying to add collection '${collection.name}'`
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
async patchCollection (collectionId: number, collection: CollectionUpdateDto) {
|
||||
try {
|
||||
await this.http.patch(`${API_COLLECTIONS}/${collectionId}`, collection)
|
||||
} catch (e) {
|
||||
let msg = `An error occurred while trying to update collection '${collectionId}'`
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
async deleteCollection (collectionId: number) {
|
||||
try {
|
||||
await this.http.delete(`${API_COLLECTIONS}/${collectionId}`)
|
||||
} catch (e) {
|
||||
let msg = `An error occurred while trying to delete collection '${collectionId}'`
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
async getSeries (collectionId: number, pageRequest?: PageRequest): Promise<SeriesDto[]> {
|
||||
try {
|
||||
return (await this.http.get(`${API_COLLECTIONS}/${collectionId}/series`)).data
|
||||
} catch (e) {
|
||||
let msg = 'An error occurred while trying to retrieve series'
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -100,6 +100,18 @@ export default class KomgaSeriesService {
|
|||
}
|
||||
}
|
||||
|
||||
async getCollections (seriesId: number): Promise<CollectionDto[]> {
|
||||
try {
|
||||
return (await this.http.get(`${API_SERIES}/${seriesId}/collections`)).data
|
||||
} catch (e) {
|
||||
let msg = 'An error occurred while trying to retrieve collections'
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
async analyzeSeries (series: SeriesDto) {
|
||||
try {
|
||||
await this.http.post(`${API_SERIES}/${series.id}/analyze`)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,18 @@
|
|||
import { bookThumbnailUrl, seriesThumbnailUrl } from '@/functions/urls'
|
||||
import { bookThumbnailUrl, collectionThumbnailUrl, seriesThumbnailUrl } from '@/functions/urls'
|
||||
import { VueRouter } from 'vue-router/types/router'
|
||||
|
||||
function plural (count: number, singular: string, plural: string) {
|
||||
return `${count} ${count === 1 ? singular : plural}`
|
||||
}
|
||||
|
||||
export function createItem (item: BookDto | SeriesDto): Item<BookDto | SeriesDto> {
|
||||
if ('seriesId' in item) {
|
||||
export enum ItemTypes {
|
||||
BOOK, SERIES, COLLECTION
|
||||
}
|
||||
|
||||
export function createItem (item: BookDto | SeriesDto | CollectionDto): Item<BookDto | SeriesDto | CollectionDto> {
|
||||
if ('seriesIds' in item) {
|
||||
return new CollectionItem(item)
|
||||
} else if ('seriesId' in item) {
|
||||
return new BookItem(item)
|
||||
} else if ('libraryId' in item) {
|
||||
return new SeriesItem(item)
|
||||
|
|
@ -16,7 +22,7 @@ export function createItem (item: BookDto | SeriesDto): Item<BookDto | SeriesDto
|
|||
}
|
||||
|
||||
export abstract class Item<T> {
|
||||
item: T;
|
||||
item: T
|
||||
|
||||
constructor (item: T) {
|
||||
this.item = item
|
||||
|
|
@ -30,13 +36,15 @@ export abstract class Item<T> {
|
|||
}
|
||||
}
|
||||
|
||||
abstract thumbnailUrl(): string
|
||||
abstract type (): ItemTypes
|
||||
|
||||
abstract title(): string
|
||||
abstract thumbnailUrl (): string
|
||||
|
||||
abstract body(): string
|
||||
abstract title (): string
|
||||
|
||||
abstract goto(router: VueRouter): void
|
||||
abstract body (): string
|
||||
|
||||
abstract goto (router: VueRouter): void
|
||||
}
|
||||
|
||||
export class BookItem extends Item<BookDto> {
|
||||
|
|
@ -44,6 +52,10 @@ export class BookItem extends Item<BookDto> {
|
|||
return bookThumbnailUrl(this.item.id)
|
||||
}
|
||||
|
||||
type (): ItemTypes {
|
||||
return ItemTypes.BOOK
|
||||
}
|
||||
|
||||
title (): string {
|
||||
const m = this.item.metadata
|
||||
return `#${m.number} - ${m.title}`
|
||||
|
|
@ -51,10 +63,7 @@ export class BookItem extends Item<BookDto> {
|
|||
|
||||
body (): string {
|
||||
let c = this.item.media.pagesCount
|
||||
return `
|
||||
<div>${this.item.size}</div>
|
||||
<div>${plural(c, 'Page', 'Pages')}</div>
|
||||
`
|
||||
return `<span>${plural(c, 'Page', 'Pages')}</span>`
|
||||
}
|
||||
|
||||
goto (router: VueRouter): void {
|
||||
|
|
@ -67,6 +76,10 @@ export class SeriesItem extends Item<SeriesDto> {
|
|||
return seriesThumbnailUrl(this.item.id)
|
||||
}
|
||||
|
||||
type (): ItemTypes {
|
||||
return ItemTypes.SERIES
|
||||
}
|
||||
|
||||
title (): string {
|
||||
return this.item.metadata.title
|
||||
}
|
||||
|
|
@ -80,3 +93,26 @@ export class SeriesItem extends Item<SeriesDto> {
|
|||
router.push({ name: 'browse-series', params: { seriesId: this.item.id.toString() } })
|
||||
}
|
||||
}
|
||||
|
||||
export class CollectionItem extends Item<CollectionDto> {
|
||||
thumbnailUrl (): string {
|
||||
return collectionThumbnailUrl(this.item.id)
|
||||
}
|
||||
|
||||
type (): ItemTypes {
|
||||
return ItemTypes.COLLECTION
|
||||
}
|
||||
|
||||
title (): string {
|
||||
return this.item.name
|
||||
}
|
||||
|
||||
body (): string {
|
||||
let c = this.item.seriesIds.length
|
||||
return `<span>${c} Series</span>`
|
||||
}
|
||||
|
||||
goto (router: VueRouter): void {
|
||||
router.push({ name: 'browse-collection', params: { collectionId: this.item.id.toString() } })
|
||||
}
|
||||
}
|
||||
|
|
|
|||
21
komga-webui/src/types/komga-collections.ts
Normal file
21
komga-webui/src/types/komga-collections.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
interface CollectionDto {
|
||||
id: number,
|
||||
name: string,
|
||||
ordered: boolean,
|
||||
filtered: boolean,
|
||||
seriesIds: number[],
|
||||
createdDate: string,
|
||||
lastModifiedDate: string
|
||||
}
|
||||
|
||||
interface CollectionCreationDto {
|
||||
name: string,
|
||||
ordered: boolean,
|
||||
seriesIds: number[]
|
||||
}
|
||||
|
||||
interface CollectionUpdateDto {
|
||||
name?: string,
|
||||
ordered?: boolean,
|
||||
seriesIds?: number[]
|
||||
}
|
||||
277
komga-webui/src/views/BrowseCollection.vue
Normal file
277
komga-webui/src/views/BrowseCollection.vue
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
<template>
|
||||
<div v-if="collection">
|
||||
<toolbar-sticky v-if="!editElements && selected.length === 0">
|
||||
|
||||
<collection-actions-menu v-if="collection"
|
||||
:collection="collection"
|
||||
@deleted="afterDelete"
|
||||
/>
|
||||
|
||||
<v-toolbar-title v-if="collection">
|
||||
<span>{{ collection.name }}</span>
|
||||
<badge class="mx-4">{{ collection.seriesIds.length }}</badge>
|
||||
<span v-if="collection.ordered"
|
||||
class="font-italic overline"
|
||||
>(manual ordering)</span>
|
||||
</v-toolbar-title>
|
||||
|
||||
<v-spacer/>
|
||||
|
||||
<v-btn icon @click="startEditElements" v-if="isAdmin">
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-icon v-on="on">mdi-playlist-edit</v-icon>
|
||||
</template>
|
||||
<span>Edit elements</span>
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-btn icon @click="editCollection" v-if="isAdmin">
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-icon v-on="on">mdi-pencil</v-icon>
|
||||
</template>
|
||||
<span>Edit collection</span>
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
</toolbar-sticky>
|
||||
|
||||
<!-- Edit elements sticky bar -->
|
||||
<v-scroll-y-transition hide-on-leave>
|
||||
<toolbar-sticky v-if="editElements" :elevation="5" color="white">
|
||||
<v-btn icon @click="cancelEditElements">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn icon color="primary" @click="doEditElements" :disabled="series.length === 0">
|
||||
<v-icon>mdi-check</v-icon>
|
||||
</v-btn>
|
||||
|
||||
</toolbar-sticky>
|
||||
</v-scroll-y-transition>
|
||||
|
||||
<!-- Selection sticky bar -->
|
||||
<v-scroll-y-transition hide-on-leave>
|
||||
<toolbar-sticky v-if="selected.length > 0" :elevation="5" color="white">
|
||||
<v-btn icon @click="selected=[]">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
<v-toolbar-title>
|
||||
<span>{{ selected.length }} selected</span>
|
||||
</v-toolbar-title>
|
||||
|
||||
<v-spacer/>
|
||||
|
||||
<v-btn icon @click="markSelectedRead()">
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-icon v-on="on">mdi-bookmark-check</v-icon>
|
||||
</template>
|
||||
<span>Mark as Read</span>
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-btn icon @click="markSelectedUnread()">
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-icon v-on="on">mdi-bookmark-remove</v-icon>
|
||||
</template>
|
||||
<span>Mark as Unread</span>
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-btn icon @click="addToCollection()">
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-icon v-on="on">mdi-playlist-plus</v-icon>
|
||||
</template>
|
||||
<span>Add to collection</span>
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-btn icon @click="dialogEdit = true" v-if="isAdmin">
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-icon v-on="on">mdi-pencil</v-icon>
|
||||
</template>
|
||||
<span>Edit metadata</span>
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
</toolbar-sticky>
|
||||
</v-scroll-y-transition>
|
||||
|
||||
<edit-series-dialog v-model="dialogEdit"
|
||||
:series.sync="selectedSeries"
|
||||
/>
|
||||
|
||||
<edit-series-dialog v-model="dialogEditSingle"
|
||||
:series.sync="editSeriesSingle"
|
||||
/>
|
||||
|
||||
<collection-add-to-dialog v-model="dialogAddToCollection"
|
||||
:series="selectedSeries"
|
||||
/>
|
||||
|
||||
<collection-edit-dialog v-model="dialogEditCollection"
|
||||
:collection="collection"
|
||||
@updated="loadCollection(collectionId)"
|
||||
/>
|
||||
|
||||
<v-container fluid class="px-6">
|
||||
|
||||
<item-browser
|
||||
:items.sync="series"
|
||||
:selected.sync="selected"
|
||||
:edit-function="singleEdit"
|
||||
:draggable="editElements && collection.ordered"
|
||||
:deletable="editElements"
|
||||
/>
|
||||
|
||||
</v-container>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Badge from '@/components/Badge.vue'
|
||||
import CollectionActionsMenu from '@/components/CollectionActionsMenu.vue'
|
||||
import CollectionAddToDialog from '@/components/CollectionAddToDialog.vue'
|
||||
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 Vue from 'vue'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'BrowseCollection',
|
||||
components: {
|
||||
ToolbarSticky,
|
||||
ItemBrowser,
|
||||
EditSeriesDialog,
|
||||
CollectionAddToDialog,
|
||||
CollectionEditDialog,
|
||||
CollectionActionsMenu,
|
||||
Badge,
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
collection: undefined as CollectionDto | undefined,
|
||||
series: [] as SeriesDto[],
|
||||
seriesCopy: [] as SeriesDto[],
|
||||
selectedSeries: [] as SeriesDto[],
|
||||
editSeriesSingle: {} as SeriesDto,
|
||||
selected: [],
|
||||
dialogEdit: false,
|
||||
dialogEditSingle: false,
|
||||
dialogAddToCollection: false,
|
||||
dialogEditCollection: false,
|
||||
editElements: false,
|
||||
}
|
||||
},
|
||||
props: {
|
||||
collectionId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
selected (val: number[]) {
|
||||
this.selectedSeries = val.map(id => this.series.find(s => s.id === id))
|
||||
.filter(x => x !== undefined) as SeriesDto[]
|
||||
},
|
||||
selectedSeries (val: SeriesDto[]) {
|
||||
val.forEach(s => {
|
||||
let index = this.series.findIndex(x => x.id === s.id)
|
||||
if (index !== -1) {
|
||||
this.series.splice(index, 1, s)
|
||||
}
|
||||
index = this.seriesCopy.findIndex(x => x.id === s.id)
|
||||
if (index !== -1) {
|
||||
this.seriesCopy.splice(index, 1, s)
|
||||
}
|
||||
})
|
||||
},
|
||||
editSeriesSingle (val: SeriesDto) {
|
||||
let index = this.series.findIndex(x => x.id === val.id)
|
||||
if (index !== -1) {
|
||||
this.series.splice(index, 1, val)
|
||||
}
|
||||
index = this.seriesCopy.findIndex(x => x.id === val.id)
|
||||
if (index !== -1) {
|
||||
this.seriesCopy.splice(index, 1, val)
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.loadCollection(this.collectionId)
|
||||
},
|
||||
beforeRouteUpdate (to, from, next) {
|
||||
if (to.params.collectionId !== from.params.collectionId) {
|
||||
// reset
|
||||
this.series = []
|
||||
this.editElements = false
|
||||
|
||||
this.loadCollection(Number(to.params.collectionId))
|
||||
}
|
||||
|
||||
next()
|
||||
},
|
||||
computed: {
|
||||
isAdmin (): boolean {
|
||||
return this.$store.getters.meAdmin
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async loadCollection (collectionId: number) {
|
||||
this.collection = await this.$komgaCollections.getOneCollection(collectionId)
|
||||
this.series = await this.$komgaCollections.getSeries(collectionId)
|
||||
this.seriesCopy = [...this.series]
|
||||
},
|
||||
singleEdit (series: SeriesDto) {
|
||||
this.editSeriesSingle = series
|
||||
this.dialogEditSingle = true
|
||||
},
|
||||
async markSelectedRead () {
|
||||
await Promise.all(this.selectedSeries.map(s =>
|
||||
this.$komgaSeries.markAsRead(s.id),
|
||||
))
|
||||
this.selectedSeries = await Promise.all(this.selectedSeries.map(s =>
|
||||
this.$komgaSeries.getOneSeries(s.id),
|
||||
))
|
||||
},
|
||||
async markSelectedUnread () {
|
||||
await Promise.all(this.selectedSeries.map(s =>
|
||||
this.$komgaSeries.markAsUnread(s.id),
|
||||
))
|
||||
this.selectedSeries = await Promise.all(this.selectedSeries.map(s =>
|
||||
this.$komgaSeries.getOneSeries(s.id),
|
||||
))
|
||||
},
|
||||
addToCollection () {
|
||||
this.dialogAddToCollection = true
|
||||
},
|
||||
startEditElements () {
|
||||
this.editElements = true
|
||||
},
|
||||
cancelEditElements () {
|
||||
this.editElements = false
|
||||
this.series = [...this.seriesCopy]
|
||||
},
|
||||
doEditElements () {
|
||||
this.editElements = false
|
||||
const update = {
|
||||
seriesIds: this.series.map(x => x.id),
|
||||
} as CollectionUpdateDto
|
||||
this.$komgaCollections.patchCollection(this.collectionId, update)
|
||||
this.loadCollection(this.collectionId)
|
||||
},
|
||||
editCollection () {
|
||||
this.dialogEditCollection = true
|
||||
},
|
||||
afterDelete () {
|
||||
this.$router.push({ name: 'browse-collections', params: { libraryId: '0' } })
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
92
komga-webui/src/views/BrowseCollections.vue
Normal file
92
komga-webui/src/views/BrowseCollections.vue
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
<template>
|
||||
<div :style="$vuetify.breakpoint.name === 'xs' ? 'margin-bottom: 56px' : undefined">
|
||||
<toolbar-sticky>
|
||||
<!-- Action menu -->
|
||||
<library-actions-menu v-if="library"
|
||||
:library="library"/>
|
||||
|
||||
<v-toolbar-title>
|
||||
<span>{{ library ? library.name : 'All libraries' }}</span>
|
||||
<badge class="ml-4">{{ collections.length }}</badge>
|
||||
</v-toolbar-title>
|
||||
</toolbar-sticky>
|
||||
|
||||
<library-navigation :libraryId="libraryId"/>
|
||||
|
||||
<v-container fluid class="px-6">
|
||||
<item-browser
|
||||
:items="collections"
|
||||
:selectable="false"
|
||||
class="px-4"
|
||||
/>
|
||||
|
||||
</v-container>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Badge from '@/components/Badge.vue'
|
||||
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 Vue from 'vue'
|
||||
|
||||
const cookiePageSize = 'pagesize'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'BrowseCollections',
|
||||
components: {
|
||||
LibraryActionsMenu,
|
||||
ToolbarSticky,
|
||||
LibraryNavigation,
|
||||
ItemBrowser,
|
||||
Badge,
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
library: undefined as LibraryDto | undefined,
|
||||
collections: [] as CollectionDto[],
|
||||
}
|
||||
},
|
||||
props: {
|
||||
libraryId: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.loadLibrary(this.libraryId)
|
||||
},
|
||||
beforeRouteUpdate (to, from, next) {
|
||||
if (to.params.libraryId !== from.params.libraryId) {
|
||||
// reset
|
||||
this.collections = []
|
||||
|
||||
this.loadLibrary(Number(to.params.libraryId))
|
||||
}
|
||||
|
||||
next()
|
||||
},
|
||||
computed: {
|
||||
isAdmin (): boolean {
|
||||
return this.$store.getters.meAdmin
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async loadLibrary (libraryId: number) {
|
||||
this.library = this.getLibraryLazy(libraryId)
|
||||
const lib = libraryId !== 0 ? [libraryId] : undefined
|
||||
this.collections = await this.$komgaCollections.getCollections(lib)
|
||||
},
|
||||
getLibraryLazy (libraryId: any): LibraryDto | undefined {
|
||||
if (libraryId !== 0) {
|
||||
return this.$store.getters.getLibraryById(libraryId)
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div>
|
||||
<div :style="$vuetify.breakpoint.name === 'xs' ? 'margin-bottom: 56px' : undefined">
|
||||
<toolbar-sticky v-if="selected.length === 0">
|
||||
<!-- Action menu -->
|
||||
<library-actions-menu v-if="library"
|
||||
|
|
@ -57,12 +57,30 @@
|
|||
</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-btn icon @click="addToCollection()">
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-icon v-on="on">mdi-playlist-plus</v-icon>
|
||||
</template>
|
||||
<span>Add to collection</span>
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-btn icon @click="dialogEdit = true" v-if="isAdmin">
|
||||
<v-icon>mdi-pencil</v-icon>
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-icon v-on="on">mdi-pencil</v-icon>
|
||||
</template>
|
||||
<span>Edit metadata</span>
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
</toolbar-sticky>
|
||||
</v-scroll-y-transition>
|
||||
|
||||
<library-navigation v-if="collections.length > 0"
|
||||
:libraryId="libraryId"
|
||||
/>
|
||||
|
||||
<edit-series-dialog v-model="dialogEdit"
|
||||
:series.sync="selectedSeries"
|
||||
/>
|
||||
|
|
@ -71,6 +89,10 @@
|
|||
:series.sync="editSeriesSingle"
|
||||
/>
|
||||
|
||||
<collection-add-to-dialog v-model="dialogAddToCollection"
|
||||
:series="selectedSeries"
|
||||
/>
|
||||
|
||||
<v-container fluid class="px-6">
|
||||
<empty-state
|
||||
v-if="totalPages === 0"
|
||||
|
|
@ -102,11 +124,13 @@
|
|||
|
||||
<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'
|
||||
import ItemBrowser from '@/components/ItemBrowser.vue'
|
||||
import LibraryActionsMenu from '@/components/LibraryActionsMenu.vue'
|
||||
import LibraryNavigation from '@/components/LibraryNavigation.vue'
|
||||
import PageSizeSelect from '@/components/PageSizeSelect.vue'
|
||||
import SortMenuButton from '@/components/SortMenuButton.vue'
|
||||
import ToolbarSticky from '@/components/ToolbarSticky.vue'
|
||||
|
|
@ -128,7 +152,9 @@ export default Vue.extend({
|
|||
Badge,
|
||||
EditSeriesDialog,
|
||||
ItemBrowser,
|
||||
CollectionAddToDialog,
|
||||
PageSizeSelect,
|
||||
LibraryNavigation,
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
|
|
@ -156,6 +182,8 @@ export default Vue.extend({
|
|||
selected: [],
|
||||
dialogEdit: false,
|
||||
dialogEditSingle: false,
|
||||
dialogAddToCollection: false,
|
||||
collections: [] as CollectionDto[],
|
||||
}
|
||||
},
|
||||
props: {
|
||||
|
|
@ -212,6 +240,7 @@ export default Vue.extend({
|
|||
this.totalPages = 1
|
||||
this.totalElements = null
|
||||
this.series = []
|
||||
this.collections = []
|
||||
|
||||
this.loadLibrary(Number(to.params.libraryId))
|
||||
|
||||
|
|
@ -271,6 +300,10 @@ export default Vue.extend({
|
|||
},
|
||||
async loadLibrary (libraryId: number) {
|
||||
this.library = this.getLibraryLazy(libraryId)
|
||||
|
||||
const lib = libraryId !== 0 ? [libraryId] : undefined
|
||||
this.collections = await this.$komgaCollections.getCollections(lib)
|
||||
|
||||
await this.loadPage(libraryId, this.page, this.sortActive)
|
||||
},
|
||||
parseQuerySortOrDefault (querySort: any): SortActive {
|
||||
|
|
@ -337,6 +370,9 @@ export default Vue.extend({
|
|||
this.$komgaSeries.getOneSeries(s.id),
|
||||
))
|
||||
},
|
||||
addToCollection () {
|
||||
this.dialogAddToCollection = true
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -9,30 +9,12 @@
|
|||
<v-icon>mdi-arrow-left</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<!-- Action menu -->
|
||||
<v-menu offset-y>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn icon v-on="on">
|
||||
<v-icon>mdi-dots-vertical</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item @click="analyze()" v-if="isAdmin">
|
||||
<v-list-item-title>Analyze</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="refreshMetadata()" v-if="isAdmin">
|
||||
<v-list-item-title>Refresh metadata</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
@click="markRead()">
|
||||
<v-list-item-title>Mark as read</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
@click="markUnread()">
|
||||
<v-list-item-title>Mark as unread</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<series-actions-menu v-if="series"
|
||||
:series.sync="series"
|
||||
@add-to-collection="addToCollection"
|
||||
@mark-read="markRead"
|
||||
@mark-unread="markUnread"
|
||||
/>
|
||||
|
||||
<v-toolbar-title>
|
||||
<span v-if="$_.get(series, 'metadata.title')">{{ series.metadata.title }}</span>
|
||||
|
|
@ -128,6 +110,35 @@
|
|||
series.metadata.status.toLowerCase() }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-expansion-panels v-model="collectionPanel">
|
||||
<v-expansion-panel v-for="(c, i) in collections"
|
||||
:key="i"
|
||||
>
|
||||
<v-expansion-panel-header>{{ c.name }} collection</v-expansion-panel-header>
|
||||
<v-expansion-panel-content>
|
||||
<horizontal-scroller>
|
||||
<template v-slot:prepend>
|
||||
<router-link class="overline"
|
||||
:to="{name: 'browse-collection', params: {collectionId: c.id}}"
|
||||
>Manage collection
|
||||
</router-link>
|
||||
</template>
|
||||
<template v-slot:content>
|
||||
<div v-for="(s, i) in collectionsContent[c.id]"
|
||||
:key="i"
|
||||
>
|
||||
<item-card class="ma-2 card" :item="s"/>
|
||||
</div>
|
||||
</template>
|
||||
</horizontal-scroller>
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
|
|
@ -155,18 +166,25 @@
|
|||
</v-container>
|
||||
|
||||
<edit-series-dialog v-model="dialogEdit" :series.sync="series"/>
|
||||
|
||||
<collection-add-to-dialog v-model="dialogAddToCollection"
|
||||
:series="series"
|
||||
/>
|
||||
</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'
|
||||
import FilterMenuButton from '@/components/FilterMenuButton.vue'
|
||||
import HorizontalScroller from '@/components/HorizontalScroller.vue'
|
||||
import ItemBrowser from '@/components/ItemBrowser.vue'
|
||||
import ItemCard from '@/components/ItemCard.vue'
|
||||
import PageSizeSelect from '@/components/PageSizeSelect.vue'
|
||||
import SeriesActionsMenu from '@/components/SeriesActionsMenu.vue'
|
||||
import SortMenuButton from '@/components/SortMenuButton.vue'
|
||||
import ToolbarSticky from '@/components/ToolbarSticky.vue'
|
||||
import { parseQueryFilter, parseQuerySort } from '@/functions/query-params'
|
||||
|
|
@ -187,6 +205,9 @@ export default Vue.extend({
|
|||
EditBooksDialog,
|
||||
ItemBrowser,
|
||||
PageSizeSelect,
|
||||
SeriesActionsMenu,
|
||||
CollectionAddToDialog,
|
||||
HorizontalScroller,
|
||||
ItemCard,
|
||||
EmptyState,
|
||||
},
|
||||
|
|
@ -216,6 +237,10 @@ export default Vue.extend({
|
|||
selected: [],
|
||||
dialogEditBooks: false,
|
||||
dialogEditBookSingle: false,
|
||||
dialogAddToCollection: false,
|
||||
collections: [] as CollectionDto[],
|
||||
collectionsContent: [] as any[][],
|
||||
collectionPanel: -1,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -296,6 +321,8 @@ export default Vue.extend({
|
|||
this.totalPages = 1
|
||||
this.totalElements = null
|
||||
this.books = []
|
||||
this.collections = []
|
||||
this.collectionPanel = -1
|
||||
|
||||
this.loadSeries(Number(to.params.seriesId))
|
||||
|
||||
|
|
@ -337,6 +364,10 @@ export default Vue.extend({
|
|||
},
|
||||
async loadSeries (seriesId: number) {
|
||||
this.series = await this.$komgaSeries.getOneSeries(seriesId)
|
||||
this.collections = await this.$komgaSeries.getCollections(seriesId)
|
||||
for (const c of this.collections) {
|
||||
this.collectionsContent[c.id] = await this.$komgaCollections.getSeries(c.id)
|
||||
}
|
||||
await this.loadPage(seriesId, this.page, this.sortActive)
|
||||
},
|
||||
parseQuerySortOrDefault (querySort: any): SortActive {
|
||||
|
|
@ -401,12 +432,13 @@ export default Vue.extend({
|
|||
))
|
||||
this.loadSeries(this.seriesId)
|
||||
},
|
||||
addToCollection () {
|
||||
this.dialogAddToCollection = true
|
||||
},
|
||||
async markRead () {
|
||||
await this.$komgaSeries.markAsRead(this.seriesId)
|
||||
this.loadSeries(this.seriesId)
|
||||
},
|
||||
async markUnread () {
|
||||
await this.$komgaSeries.markAsUnread(this.seriesId)
|
||||
this.loadSeries(this.seriesId)
|
||||
},
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue