diff --git a/komga-webui/src/components/ReorderLibraries.vue b/komga-webui/src/components/ReorderLibraries.vue index 8ed114e12..8e34a29bb 100644 --- a/komga-webui/src/components/ReorderLibraries.vue +++ b/komga-webui/src/components/ReorderLibraries.vue @@ -73,7 +73,6 @@ export default Vue.extend({ dragOptions(): any { return { animation: 200, - // group: 'item-cards', ghostClass: 'ghost', } }, diff --git a/komga-webui/src/components/dialogs/EditRecommendedDialog.vue b/komga-webui/src/components/dialogs/EditRecommendedDialog.vue new file mode 100644 index 000000000..406c3d26b --- /dev/null +++ b/komga-webui/src/components/dialogs/EditRecommendedDialog.vue @@ -0,0 +1,135 @@ + + + + {{ $t('dialog.edit_recommended.dialog_title') }} + + mdi-close + + + + + + + + mdi-drag-horizontal-variant + + + {{ $t(`dashboard.${l.section.toLowerCase()}`) }} + + + + + + + + + + + + {{ $t('common.cancel') }} + {{ + $t('dialog.edit_recommended.button_reset') + }} + + {{ + $t('dialog.edit_recommended.button_confirm') + }} + + + + + + + + + diff --git a/komga-webui/src/locales/en.json b/komga-webui/src/locales/en.json index d953cc87e..0d0840058 100644 --- a/komga-webui/src/locales/en.json +++ b/komga-webui/src/locales/en.json @@ -516,6 +516,11 @@ "tab_general": "General", "tab_poster": "Poster" }, + "edit_recommended": { + "button_confirm": "Save changes", + "button_reset": "Reset to default", + "dialog_title": "Edit recommended view" + }, "edit_series": { "button_cancel": "Cancel", "button_confirm": "Save changes", diff --git a/komga-webui/src/services/komga-settings.service.ts b/komga-webui/src/services/komga-settings.service.ts index 2ebc2782d..b35c9377c 100644 --- a/komga-webui/src/services/komga-settings.service.ts +++ b/komga-webui/src/services/komga-settings.service.ts @@ -83,4 +83,16 @@ export default class KomgaSettingsService { throw new Error(msg) } } + + async deleteClientSettingUser(settings: string[]) { + try { + await this.http.delete(`${API_CLIENT_SETTINGS}/user`, {data: settings}) + } catch (e) { + let msg = 'An error occurred while trying to delete user client setting' + if (e.response.data.message) { + msg += `: ${e.response.data.message}` + } + throw new Error(msg) + } + } } diff --git a/komga-webui/src/types/komga-clientsettings.ts b/komga-webui/src/types/komga-clientsettings.ts index 6e5ecd2d8..24fb5b034 100644 --- a/komga-webui/src/types/komga-clientsettings.ts +++ b/komga-webui/src/types/komga-clientsettings.ts @@ -19,6 +19,7 @@ export enum CLIENT_SETTING { WEBUI_POSTER_BLUR_UNREAD = 'webui.poster.blur_unread', WEBUI_LIBRARIES = 'webui.libraries', WEBUI_SERIES_GROUPS = 'webui.series_groups', + WEBUI_RECOMMENDED = 'webui.recommended', } export interface ClientSettingLibrary { @@ -112,3 +113,33 @@ export const SERIES_GROUP_JAPANESE = { 'わ': ['わ', 'ワ'], }, } as ClientSettingsSeriesGroup + +export interface ClientSettingsRecommendedView { + sections: ClientSettingsRecommendedViewSection[], +} + +export interface ClientSettingsRecommendedViewSection { + section: RecommendedViewSection, +} + +export enum RecommendedViewSection { + KEEP_READING = 'KEEP_READING', + ON_DECK = 'ON_DECK', + RECENTLY_RELEASED_BOOKS = 'RECENTLY_RELEASED_BOOKS', + RECENTLY_ADDED_BOOKS = 'RECENTLY_ADDED_BOOKS', + RECENTLY_ADDED_SERIES = 'RECENTLY_ADDED_SERIES', + RECENTLY_UPDATED_SERIES = 'RECENTLY_UPDATED_SERIES', + RECENTLY_READ_BOOKS = 'RECENTLY_READ_BOOKS', +} + +export const RECOMMENDED_DEFAULT = { + sections: [ + {section: RecommendedViewSection.KEEP_READING}, + {section: RecommendedViewSection.ON_DECK}, + {section: RecommendedViewSection.RECENTLY_RELEASED_BOOKS}, + {section: RecommendedViewSection.RECENTLY_ADDED_BOOKS}, + {section: RecommendedViewSection.RECENTLY_ADDED_SERIES}, + {section: RecommendedViewSection.RECENTLY_UPDATED_SERIES}, + {section: RecommendedViewSection.RECENTLY_READ_BOOKS}, + ], +} as ClientSettingsRecommendedView diff --git a/komga-webui/src/views/DashboardView.vue b/komga-webui/src/views/DashboardView.vue index cd9ce0216..1f1a41192 100644 --- a/komga-webui/src/views/DashboardView.vue +++ b/komga-webui/src/views/DashboardView.vue @@ -54,150 +54,57 @@ > - scrollChanged(loaderInProgressBooks, percent)" - > - - {{ $t('dashboard.keep_reading') }} - - - - - + + scrollChanged(section.loader, percent)" + > + + {{ $t(`dashboard.${section.value.toLowerCase()}`) }} + + + + + + + - scrollChanged(loaderOnDeckBooks, percent)" - > - - {{ $t('dashboard.on_deck') }} - - - - - + + + mdi-pencil + + - scrollChanged(loaderRecentlyReleasedBooks, percent)" - > - - {{ $t('dashboard.recently_released_books') }} - - - - - - - scrollChanged(loaderLatestBooks, percent)" - > - - {{ $t('dashboard.recently_added_books') }} - - - - - - - scrollChanged(loaderNewSeries, percent)" - > - - {{ $t('dashboard.recently_added_series') }} - - - - - - - scrollChanged(loaderUpdatedSeries, percent)" - > - - {{ $t('dashboard.recently_updated_series') }} - - - - - - - scrollChanged(loaderRecentlyReadBooks, percent)" - > - - {{ $t('dashboard.recently_read_books') }} - - - - - + @@ -246,10 +153,32 @@ import { SearchOperatorAfter, SearchOperatorIs, } from '@/types/komga-search' +import { + CLIENT_SETTING, + ClientSettingsRecommendedView, + ClientSettingsRecommendedViewSection, + ClientSettingUserUpdateDto, + RECOMMENDED_DEFAULT, + RecommendedViewSection, +} from '@/types/komga-clientsettings' +import EditRecommendedDialog from '@/components/dialogs/EditRecommendedDialog.vue' + +interface SectionConfig { + loader: PageLoader | undefined, + type: SectionType, + value: RecommendedViewSection, + itemContext?: ItemContext[] | undefined, +} + +enum SectionType { + SERIES, + BOOK, +} export default Vue.extend({ name: 'DashboardView', components: { + EditRecommendedDialog, HorizontalScroller, EmptyState, ToolbarSticky, @@ -261,14 +190,16 @@ export default Vue.extend({ data: () => { return { ItemContext, + SectionType, loading: false, - loaderNewSeries: undefined as unknown as PageLoader, - loaderUpdatedSeries: undefined as unknown as PageLoader, - loaderLatestBooks: undefined as unknown as PageLoader, - loaderInProgressBooks: undefined as unknown as PageLoader, - loaderOnDeckBooks: undefined as unknown as PageLoader, - loaderRecentlyReleasedBooks: undefined as unknown as PageLoader, - loaderRecentlyReadBooks: undefined as unknown as PageLoader, + modalEditRecommended: false, + loaderRecentlyAddedSeries: undefined as PageLoader | undefined, + loaderRecentlyUpdatedSeries: undefined as PageLoader | undefined, + loaderRecentlyAddedBooks: undefined as PageLoader | undefined, + loaderKeepReadingBooks: undefined as PageLoader | undefined, + loaderOnDeckBooks: undefined as PageLoader | undefined, + loaderRecentlyReleasedBooks: undefined as PageLoader | undefined, + loaderRecentlyReadBooks: undefined as PageLoader | undefined, selectedSeries: [] as SeriesDto[], selectedBooks: [] as BookDto[], } @@ -312,10 +243,6 @@ export default Vue.extend({ }, }, watch: { - libraryId(val) { - this.setupLoaders(val) - this.loadAll() - }, libraryIds() { this.setupLoaders(this.libraryId) this.loadAll() @@ -333,6 +260,36 @@ export default Vue.extend({ }, }, computed: { + settingsKey(): string { + return `${CLIENT_SETTING.WEBUI_RECOMMENDED}.${this.libraryId}`.toLowerCase() + }, + viewConfig: { + get: function (): ClientSettingsRecommendedView { + try { + return JSON.parse(this.$store.getters.getClientSettings[this.settingsKey].value) as ClientSettingsRecommendedView + } catch (_) { + } + return RECOMMENDED_DEFAULT + }, + set: async function (view: ClientSettingsRecommendedView) { + let newSettings = {} as Record + newSettings[this.settingsKey] = { + value: JSON.stringify(view), + } + await this.$komgaSettings.updateClientSettingUser(newSettings) + await this.$store.dispatch('getClientSettingsUser') + this.setupLoaders(this.libraryId) + this.loadAll(true) + }, + }, + sections(): SectionConfig[] { + const sections = [] as SectionConfig[] + this.viewConfig.sections.forEach((it: ClientSettingsRecommendedViewSection) => { + const config = this.getSectionConfig(it.section) + if (config != undefined) sections.push(config) + }) + return sections + }, library(): LibraryDto | undefined { return this.getLibraryLazy(this.libraryId) }, @@ -346,13 +303,13 @@ export default Vue.extend({ return this.$vuetify.breakpoint.xs ? 120 : 150 }, allEmpty(): boolean { - return this.loaderNewSeries?.items.length === 0 && - this.loaderUpdatedSeries?.items.length === 0 && - this.loaderLatestBooks?.items.length === 0 && - this.loaderInProgressBooks?.items.length === 0 && - this.loaderOnDeckBooks?.items.length === 0 && - this.loaderRecentlyReleasedBooks?.items.length === 0 && - this.loaderRecentlyReadBooks?.items.length === 0 + return (this.loaderRecentlyAddedSeries == undefined || this.loaderRecentlyAddedSeries?.items.length === 0) && + (this.loaderRecentlyUpdatedSeries == undefined || this.loaderRecentlyUpdatedSeries?.items.length === 0) && + (this.loaderRecentlyAddedBooks == undefined || this.loaderRecentlyAddedBooks?.items.length === 0) && + (this.loaderKeepReadingBooks == undefined || this.loaderKeepReadingBooks?.items.length === 0) && + (this.loaderOnDeckBooks == undefined || this.loaderOnDeckBooks?.items.length === 0) && + (this.loaderRecentlyReleasedBooks == undefined || this.loaderRecentlyReleasedBooks?.items.length === 0) && + (this.loaderRecentlyReadBooks == undefined || this.loaderRecentlyReadBooks?.items.length === 0) }, individualLibrary(): boolean { return this.libraryId !== LIBRARIES_ALL @@ -362,6 +319,68 @@ export default Vue.extend({ }, }, methods: { + async resetDefaultView() { + await this.$komgaSettings.deleteClientSettingUser([this.settingsKey]) + await this.$store.dispatch('getClientSettingsUser') + this.setupLoaders(this.libraryId) + this.loadAll(true) + }, + hasSection(section: RecommendedViewSection): boolean { + return this.viewConfig.sections.some(it => it.section === section) + }, + getSectionConfig(section: RecommendedViewSection): SectionConfig | undefined { + switch (section) { + case RecommendedViewSection.KEEP_READING: + return { + loader: this.loaderKeepReadingBooks, + type: SectionType.BOOK, + value: section, + itemContext: [ItemContext.SHOW_SERIES], + } + case RecommendedViewSection.ON_DECK: + return { + loader: this.loaderOnDeckBooks, + type: SectionType.BOOK, + value: section, + itemContext: [ItemContext.SHOW_SERIES], + } + case RecommendedViewSection.RECENTLY_RELEASED_BOOKS: + return { + loader: this.loaderRecentlyReleasedBooks, + type: SectionType.BOOK, + value: section, + itemContext: [ItemContext.RELEASE_DATE, ItemContext.SHOW_SERIES], + } + case RecommendedViewSection.RECENTLY_ADDED_BOOKS: + return { + loader: this.loaderRecentlyAddedBooks, + type: SectionType.BOOK, + value: section, + itemContext: [ItemContext.SHOW_SERIES], + } + case RecommendedViewSection.RECENTLY_ADDED_SERIES: + return { + loader: this.loaderRecentlyAddedSeries, + type: SectionType.SERIES, + value: section, + } + case RecommendedViewSection.RECENTLY_UPDATED_SERIES: + return { + loader: this.loaderRecentlyUpdatedSeries, + type: SectionType.SERIES, + value: section, + } + case RecommendedViewSection.RECENTLY_READ_BOOKS: + return { + loader: this.loaderRecentlyReadBooks, + type: SectionType.BOOK, + value: section, + itemContext: [ItemContext.SHOW_SERIES], + } + default: + return undefined + } + }, async scrollChanged(loader: PageLoader, percent: number) { if (percent > 0.95) await loader.loadNext() }, @@ -379,15 +398,15 @@ export default Vue.extend({ } }, readProgressChanged(event: ReadProgressSseDto) { - if (this.loaderInProgressBooks?.items.some(b => b.id === event.bookId)) this.reload() - else if (this.loaderLatestBooks?.items.some(b => b.id === event.bookId)) this.reload() + if (this.loaderKeepReadingBooks?.items.some(b => b.id === event.bookId)) this.reload() + else if (this.loaderRecentlyAddedBooks?.items.some(b => b.id === event.bookId)) this.reload() else if (this.loaderOnDeckBooks?.items.some(b => b.id === event.bookId)) this.reload() else if (this.loaderRecentlyReleasedBooks?.items.some(b => b.id === event.bookId)) this.reload() else if (this.loaderRecentlyReadBooks?.items.some(b => b.id === event.bookId)) this.reload() }, readProgressSeriesChanged(event: ReadProgressSeriesSseDto) { - if (this.loaderUpdatedSeries?.items.some(s => s.id === event.seriesId)) this.reload() - else if (this.loaderNewSeries?.items.some(s => s.id === event.seriesId)) this.reload() + if (this.loaderRecentlyUpdatedSeries?.items.some(s => s.id === event.seriesId)) this.reload() + else if (this.loaderRecentlyAddedSeries?.items.some(s => s.id === event.seriesId)) this.reload() }, reload: throttle(function (this: any) { this.loadAll(true) @@ -400,43 +419,43 @@ export default Vue.extend({ requestLibraries.map((it: string) => new SearchConditionLibraryId(new SearchOperatorIs(it))), )) - this.loaderInProgressBooks = new PageLoader( + this.loaderKeepReadingBooks = this.hasSection(RecommendedViewSection.KEEP_READING) ? new PageLoader( {sort: ['readProgress.readDate,desc']}, (pageable: PageRequest) => this.$komgaBooks.getBooksList({ condition: new SearchConditionAllOfBook([...baseBookConditions, new SearchConditionReadStatus(new SearchOperatorIs(ReadStatus.IN_PROGRESS))]), } as BookSearch, pageable), - ) - this.loaderOnDeckBooks = new PageLoader( + ) : undefined + this.loaderOnDeckBooks = this.hasSection(RecommendedViewSection.ON_DECK) ? new PageLoader( {}, (pageable: PageRequest) => this.$komgaBooks.getBooksOnDeck(requestLibraries, pageable), - ) - this.loaderLatestBooks = new PageLoader( + ) : undefined + this.loaderRecentlyAddedBooks = this.hasSection(RecommendedViewSection.RECENTLY_ADDED_BOOKS) ? new PageLoader( {sort: ['createdDate,desc']}, (pageable: PageRequest) => this.$komgaBooks.getBooksList({ condition: new SearchConditionAllOfBook(baseBookConditions), } as BookSearch, pageable), - ) - this.loaderRecentlyReleasedBooks = new PageLoader( + ) : undefined + this.loaderRecentlyReleasedBooks = this.hasSection(RecommendedViewSection.RECENTLY_RELEASED_BOOKS) ? new PageLoader( {sort: ['metadata.releaseDate,desc']}, (pageable: PageRequest) => this.$komgaBooks.getBooksList({ condition: new SearchConditionAllOfBook([...baseBookConditions, new SearchConditionReleaseDate(new SearchOperatorAfter(subMonths(new Date(), 1)))]), } as BookSearch, pageable), - ) - this.loaderRecentlyReadBooks = new PageLoader( + ) : undefined + this.loaderRecentlyReadBooks = this.hasSection(RecommendedViewSection.RECENTLY_READ_BOOKS) ? new PageLoader( {sort: ['readProgress.readDate,desc']}, (pageable: PageRequest) => this.$komgaBooks.getBooksList({ condition: new SearchConditionAllOfBook([...baseBookConditions, new SearchConditionReadStatus(new SearchOperatorIs(ReadStatus.READ))]), } as BookSearch, pageable), - ) + ) : undefined - this.loaderNewSeries = new PageLoader( + this.loaderRecentlyAddedSeries = this.hasSection(RecommendedViewSection.RECENTLY_ADDED_SERIES) ? new PageLoader( {}, (pageable: PageRequest) => this.$komgaSeries.getNewSeries(requestLibraries, false, pageable), - ) - this.loaderUpdatedSeries = new PageLoader( + ) : undefined + this.loaderRecentlyUpdatedSeries = this.hasSection(RecommendedViewSection.RECENTLY_UPDATED_SERIES) ? new PageLoader( {}, (pageable: PageRequest) => this.$komgaSeries.getUpdatedSeries(requestLibraries, false, pageable), - ) + ) : undefined }, loadAll(reload: boolean = false) { this.loading = true @@ -446,25 +465,25 @@ export default Vue.extend({ if (reload) { Promise.all([ - this.loaderInProgressBooks.reload(), - this.loaderOnDeckBooks.reload(), - this.loaderRecentlyReleasedBooks.reload(), - this.loaderLatestBooks.reload(), - this.loaderNewSeries.reload(), - this.loaderUpdatedSeries.reload(), - this.loaderRecentlyReadBooks.reload(), + this.loaderKeepReadingBooks?.reload(), + this.loaderOnDeckBooks?.reload(), + this.loaderRecentlyReleasedBooks?.reload(), + this.loaderRecentlyAddedBooks?.reload(), + this.loaderRecentlyAddedSeries?.reload(), + this.loaderRecentlyUpdatedSeries?.reload(), + this.loaderRecentlyReadBooks?.reload(), ]).then(() => { this.loading = false }) } else { Promise.all([ - this.loaderInProgressBooks.loadNext(), - this.loaderOnDeckBooks.loadNext(), - this.loaderRecentlyReleasedBooks.loadNext(), - this.loaderLatestBooks.loadNext(), - this.loaderNewSeries.loadNext(), - this.loaderUpdatedSeries.loadNext(), - this.loaderRecentlyReadBooks.loadNext(), + this.loaderKeepReadingBooks?.loadNext(), + this.loaderOnDeckBooks?.loadNext(), + this.loaderRecentlyReleasedBooks?.loadNext(), + this.loaderRecentlyAddedBooks?.loadNext(), + this.loaderRecentlyAddedSeries?.loadNext(), + this.loaderRecentlyUpdatedSeries?.loadNext(), + this.loaderRecentlyReadBooks?.loadNext(), ]).then(() => { this.loading = false })