From 51ea41fc35db45cfff9664f8553f87c6a8221e1c Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Mon, 16 Aug 2021 16:08:56 -0700 Subject: [PATCH] Collection Redesign (#500) * Setup UI for the collection redesign. * Implemented collection details page --- Kavita.Common/EnvironmentInfo/BuildInfo.cs | 4 +- UI/Web/src/app/admin/admin-routing.module.ts | 2 +- UI/Web/src/app/app-routing.module.ts | 17 +-- UI/Web/src/app/app.module.ts | 4 +- .../edit-collection-tags.component.ts | 4 +- UI/Web/src/app/cards/cards.module.ts | 3 +- .../all-collections.component.html | 0 .../all-collections.component.scss | 0 .../all-collections.component.ts | 25 ++-- .../collection-detail.component.html | 56 ++++++++ .../collection-detail.component.scss | 20 +++ .../collection-detail.component.ts | 130 ++++++++++++++++++ .../collections/collections-routing.module.ts | 24 ++++ .../src/app/collections/collections.module.ts | 23 ++++ UI/Web/src/app/library/library.component.ts | 9 +- .../series-detail.component.scss | 10 +- 16 files changed, 289 insertions(+), 42 deletions(-) rename UI/Web/src/app/{ => collections}/all-collections/all-collections.component.html (100%) rename UI/Web/src/app/{ => collections}/all-collections/all-collections.component.scss (100%) rename UI/Web/src/app/{ => collections}/all-collections/all-collections.component.ts (79%) create mode 100644 UI/Web/src/app/collections/collection-detail/collection-detail.component.html create mode 100644 UI/Web/src/app/collections/collection-detail/collection-detail.component.scss create mode 100644 UI/Web/src/app/collections/collection-detail/collection-detail.component.ts create mode 100644 UI/Web/src/app/collections/collections-routing.module.ts create mode 100644 UI/Web/src/app/collections/collections.module.ts diff --git a/Kavita.Common/EnvironmentInfo/BuildInfo.cs b/Kavita.Common/EnvironmentInfo/BuildInfo.cs index a1f72195c..b6403b729 100644 --- a/Kavita.Common/EnvironmentInfo/BuildInfo.cs +++ b/Kavita.Common/EnvironmentInfo/BuildInfo.cs @@ -20,7 +20,7 @@ namespace Kavita.Common.EnvironmentInfo var config = attributes.OfType().FirstOrDefault(); if (config != null) { - Branch = config.Configuration; // TODO: This is not helpful, better to have main/develop branch + Branch = config.Configuration; // NOTE: This is not helpful, better to have main/develop branch } Release = $"{Version}-{Branch}"; @@ -53,4 +53,4 @@ namespace Kavita.Common.EnvironmentInfo } } } -} \ No newline at end of file +} diff --git a/UI/Web/src/app/admin/admin-routing.module.ts b/UI/Web/src/app/admin/admin-routing.module.ts index 166b8706a..a29927171 100644 --- a/UI/Web/src/app/admin/admin-routing.module.ts +++ b/UI/Web/src/app/admin/admin-routing.module.ts @@ -16,7 +16,7 @@ const routes: Routes = [ @NgModule({ - imports: [RouterModule.forChild(routes), ], + imports: [RouterModule.forChild(routes)], exports: [RouterModule] }) export class AdminRoutingModule { } diff --git a/UI/Web/src/app/app-routing.module.ts b/UI/Web/src/app/app-routing.module.ts index 0c7fb6406..b52054c49 100644 --- a/UI/Web/src/app/app-routing.module.ts +++ b/UI/Web/src/app/app-routing.module.ts @@ -1,6 +1,5 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; -import { AllCollectionsComponent } from './all-collections/all-collections.component'; import { HomeComponent } from './home/home.component'; import { LibraryDetailComponent } from './library-detail/library-detail.component'; import { LibraryComponent } from './library/library.component'; @@ -21,6 +20,10 @@ const routes: Routes = [ path: 'admin', loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule) }, + { + path: 'collections', + loadChildren: () => import('./collections/collections.module').then(m => m.CollectionsModule) + }, {path: 'library', component: LibraryComponent}, { path: '', @@ -29,14 +32,6 @@ const routes: Routes = [ children: [ {path: 'library/:id', component: LibraryDetailComponent}, {path: 'library/:libraryId/series/:seriesId', component: SeriesDetailComponent}, - { - path: 'library/:libraryId/series/:seriesId/manga', - loadChildren: () => import('../app/manga-reader/manga-reader.module').then(m => m.MangaReaderModule) - }, - { - path: 'library/:libraryId/series/:seriesId/book', - loadChildren: () => import('../app/book-reader/book-reader.module').then(m => m.BookReaderModule) - } ] }, { @@ -46,12 +41,10 @@ const routes: Routes = [ children: [ {path: 'recently-added', component: RecentlyAddedComponent}, {path: 'in-progress', component: InProgressComponent}, - {path: 'collections', component: AllCollectionsComponent}, - {path: 'collections/:id', component: AllCollectionsComponent}, + {path: 'preferences', component: UserPreferencesComponent}, ] }, {path: 'login', component: UserLoginComponent}, - {path: 'preferences', component: UserPreferencesComponent}, {path: 'no-connection', component: NotConnectedComponent}, {path: '**', component: HomeComponent, pathMatch: 'full'} ]; diff --git a/UI/Web/src/app/app.module.ts b/UI/Web/src/app/app.module.ts index 61bb92cb8..946845ba1 100644 --- a/UI/Web/src/app/app.module.ts +++ b/UI/Web/src/app/app.module.ts @@ -33,10 +33,10 @@ import { RewriteFrames as RewriteFramesIntegration } from '@sentry/integrations' import { Dedupe as DedupeIntegration } from '@sentry/integrations'; import { PersonBadgeComponent } from './person-badge/person-badge.component'; import { TypeaheadModule } from './typeahead/typeahead.module'; -import { AllCollectionsComponent } from './all-collections/all-collections.component'; import { RecentlyAddedComponent } from './recently-added/recently-added.component'; import { InProgressComponent } from './in-progress/in-progress.component'; import { CardsModule } from './cards/cards.module'; +import { CollectionsModule } from './collections/collections.module'; let sentryProviders: any[] = []; @@ -96,7 +96,6 @@ if (environment.production) { UserPreferencesComponent, // Move into SettingsModule ReviewSeriesModalComponent, PersonBadgeComponent, - AllCollectionsComponent, RecentlyAddedComponent, InProgressComponent, ], @@ -122,6 +121,7 @@ if (environment.production) { CarouselModule, TypeaheadModule, CardsModule, + CollectionsModule, ToastrModule.forRoot({ positionClass: 'toast-bottom-right', diff --git a/UI/Web/src/app/cards/_modals/edit-collection-tags/edit-collection-tags.component.ts b/UI/Web/src/app/cards/_modals/edit-collection-tags/edit-collection-tags.component.ts index 8174c739f..283b14cb8 100644 --- a/UI/Web/src/app/cards/_modals/edit-collection-tags/edit-collection-tags.component.ts +++ b/UI/Web/src/app/cards/_modals/edit-collection-tags/edit-collection-tags.component.ts @@ -105,7 +105,7 @@ export class EditCollectionTagsComponent implements OnInit { } close() { - this.modal.close(false); + this.modal.dismiss(); } async save() { @@ -135,7 +135,7 @@ export class EditCollectionTagsComponent implements OnInit { get someSelected() { const selected = this.selections.selected(); - return (selected.length != this.series.length && selected.length != 0); + return (selected.length !== this.series.length && selected.length !== 0); } updateSelectedIndex(index: number) { diff --git a/UI/Web/src/app/cards/cards.module.ts b/UI/Web/src/app/cards/cards.module.ts index 1390fc208..c9d427307 100644 --- a/UI/Web/src/app/cards/cards.module.ts +++ b/UI/Web/src/app/cards/cards.module.ts @@ -38,7 +38,7 @@ import { CardDetailsModalComponent } from './_modals/card-details-modal/card-det ], imports: [ CommonModule, - BrowserModule, + //BrowserModule, RouterModule, ReactiveFormsModule, FormsModule, // EditCollectionsModal @@ -48,7 +48,6 @@ import { CardDetailsModalComponent } from './_modals/card-details-modal/card-det NgbNavModule, NgbTooltipModule, // Card item - //NgbAccordionModule, NgbCollapseModule, NgbNavModule, //Series Detail diff --git a/UI/Web/src/app/all-collections/all-collections.component.html b/UI/Web/src/app/collections/all-collections/all-collections.component.html similarity index 100% rename from UI/Web/src/app/all-collections/all-collections.component.html rename to UI/Web/src/app/collections/all-collections/all-collections.component.html diff --git a/UI/Web/src/app/all-collections/all-collections.component.scss b/UI/Web/src/app/collections/all-collections/all-collections.component.scss similarity index 100% rename from UI/Web/src/app/all-collections/all-collections.component.scss rename to UI/Web/src/app/collections/all-collections/all-collections.component.scss diff --git a/UI/Web/src/app/all-collections/all-collections.component.ts b/UI/Web/src/app/collections/all-collections/all-collections.component.ts similarity index 79% rename from UI/Web/src/app/all-collections/all-collections.component.ts rename to UI/Web/src/app/collections/all-collections/all-collections.component.ts index ba76c76f4..d696b04bc 100644 --- a/UI/Web/src/app/all-collections/all-collections.component.ts +++ b/UI/Web/src/app/collections/all-collections/all-collections.component.ts @@ -3,13 +3,15 @@ import { Title } from '@angular/platform-browser'; import { ActivatedRoute, Router } from '@angular/router'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { ToastrService } from 'ngx-toastr'; -import { EditCollectionTagsComponent } from '../cards/_modals/edit-collection-tags/edit-collection-tags.component'; -import { CollectionTag } from '../_models/collection-tag'; -import { Pagination } from '../_models/pagination'; -import { Series } from '../_models/series'; -import { Action, ActionFactoryService, ActionItem } from '../_services/action-factory.service'; -import { CollectionTagService } from '../_services/collection-tag.service'; -import { SeriesService } from '../_services/series.service'; +import { EditCollectionTagsComponent } from 'src/app/cards/_modals/edit-collection-tags/edit-collection-tags.component'; +import { CollectionTag } from 'src/app/_models/collection-tag'; +import { Pagination } from 'src/app/_models/pagination'; +import { Series } from 'src/app/_models/series'; +import { ActionItem, ActionFactoryService, Action } from 'src/app/_services/action-factory.service'; +import { CollectionTagService } from 'src/app/_services/collection-tag.service'; +import { ImageService } from 'src/app/_services/image.service'; +import { SeriesService } from 'src/app/_services/series.service'; + /** * This component is used as a standard layout for any card detail. ie) series, in-progress, collections, etc. @@ -31,7 +33,7 @@ export class AllCollectionsComponent implements OnInit { constructor(private collectionService: CollectionTagService, private router: Router, private route: ActivatedRoute, private seriesService: SeriesService, private toastr: ToastrService, private actionFactoryService: ActionFactoryService, - private modalService: NgbModal, private titleService: Title) { + private modalService: NgbModal, private titleService: Title, private imageService: ImageService) { this.router.routeReuseStrategy.shouldReuseRoute = () => false; const routeId = this.route.snapshot.paramMap.get('id'); @@ -97,9 +99,10 @@ export class AllCollectionsComponent implements OnInit { case(Action.Edit): const modalRef = this.modalService.open(EditCollectionTagsComponent, { size: 'lg', scrollable: true }); modalRef.componentInstance.tag = collectionTag; - modalRef.closed.subscribe((reloadNeeded: boolean) => { - if (reloadNeeded) { - this.loadPage(); + modalRef.closed.subscribe((results: {success: boolean, coverImageUpdated: boolean}) => { + this.loadPage(); + if (results.coverImageUpdated) { + collectionTag.coverImage = this.imageService.randomize(this.imageService.getCollectionCoverImage(collectionTag.id)); } }); break; diff --git a/UI/Web/src/app/collections/collection-detail/collection-detail.component.html b/UI/Web/src/app/collections/collection-detail/collection-detail.component.html new file mode 100644 index 000000000..ff77357ef --- /dev/null +++ b/UI/Web/src/app/collections/collection-detail/collection-detail.component.html @@ -0,0 +1,56 @@ +
+
+
+ +
+
+
+

+ {{collectionTag.title}} +

+
+
+ +
+ +
+
+
+ +
+
+
+
+ + + + + + + +
+
+ +
+
+
\ No newline at end of file diff --git a/UI/Web/src/app/collections/collection-detail/collection-detail.component.scss b/UI/Web/src/app/collections/collection-detail/collection-detail.component.scss new file mode 100644 index 000000000..7e13a843c --- /dev/null +++ b/UI/Web/src/app/collections/collection-detail/collection-detail.component.scss @@ -0,0 +1,20 @@ +@import '~bootstrap/scss/mixins/_breakpoints.scss'; + +.poster { + width: 100%; + max-height: 481px; + } + + + // Including breakpoint has issue with resovling variable, so copying gridpoints here + @include media-breakpoint-down(xs, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px)) { + .poster { + display: none; + } + } + + @include media-breakpoint-down(sm, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px)) { + .read-btn--text { + display: none; + } + } \ No newline at end of file diff --git a/UI/Web/src/app/collections/collection-detail/collection-detail.component.ts b/UI/Web/src/app/collections/collection-detail/collection-detail.component.ts new file mode 100644 index 000000000..8e155cf35 --- /dev/null +++ b/UI/Web/src/app/collections/collection-detail/collection-detail.component.ts @@ -0,0 +1,130 @@ +import { Component, OnInit } from '@angular/core'; +import { Title } from '@angular/platform-browser'; +import { Router, ActivatedRoute } from '@angular/router'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { ToastrService } from 'ngx-toastr'; +import { take } from 'rxjs/operators'; +import { UpdateFilterEvent } from 'src/app/cards/card-detail-layout/card-detail-layout.component'; +import { EditCollectionTagsComponent } from 'src/app/cards/_modals/edit-collection-tags/edit-collection-tags.component'; +import { UtilityService } from 'src/app/shared/_services/utility.service'; +import { CollectionTag } from 'src/app/_models/collection-tag'; +import { MangaFormat } from 'src/app/_models/manga-format'; +import { Pagination } from 'src/app/_models/pagination'; +import { Series } from 'src/app/_models/series'; +import { FilterItem, mangaFormatFilters, SeriesFilter } from 'src/app/_models/series-filter'; +import { AccountService } from 'src/app/_services/account.service'; +import { Action, ActionFactoryService, ActionItem } from 'src/app/_services/action-factory.service'; +import { CollectionTagService } from 'src/app/_services/collection-tag.service'; +import { ImageService } from 'src/app/_services/image.service'; +import { SeriesService } from 'src/app/_services/series.service'; + +@Component({ + selector: 'app-collection-detail', + templateUrl: './collection-detail.component.html', + styleUrls: ['./collection-detail.component.scss'] +}) +export class CollectionDetailComponent implements OnInit { + + collectionTag!: CollectionTag; + tagImage: string = ''; + isLoading: boolean = true; + collections: CollectionTag[] = []; + collectionTagName: string = ''; + series: Array = []; + seriesPagination!: Pagination; + collectionTagActions: ActionItem[] = []; + isAdmin: boolean = false; + filters: Array = mangaFormatFilters; + filter: SeriesFilter = { + mangaFormat: null + }; + + constructor(public imageService: ImageService, private collectionService: CollectionTagService, private router: Router, private route: ActivatedRoute, + private seriesService: SeriesService, private toastr: ToastrService, private actionFactoryService: ActionFactoryService, + private modalService: NgbModal, private titleService: Title, private accountService: AccountService, private utilityService: UtilityService) { + this.router.routeReuseStrategy.shouldReuseRoute = () => false; + + this.accountService.currentUser$.pipe(take(1)).subscribe(user => { + if (user) { + this.isAdmin = this.accountService.hasAdminRole(user); + } + }); + + const routeId = this.route.snapshot.paramMap.get('id'); + if (routeId === null) { + this.router.navigate(['collections']); + return; + } + const tagId = parseInt(routeId, 10); + this.collectionService.allTags().subscribe(tags => { + this.collections = tags; + const matchingTags = this.collections.filter(t => t.id === tagId); + if (matchingTags.length === 0) { + this.toastr.error('You don\'t have access to any libraries this tag belongs to or this tag is invalid'); + + return; + } + this.collectionTag = matchingTags[0]; + this.tagImage = this.imageService.randomize(this.imageService.getCollectionCoverImage(this.collectionTag.id)); + this.titleService.setTitle('Kavita - ' + this.collectionTag.title + ' Collection'); + this.loadPage(); + }); + } + + ngOnInit(): void { + this.collectionTagActions = this.actionFactoryService.getCollectionTagActions(this.handleCollectionActionCallback.bind(this)); + } + + onPageChange(pagination: Pagination) { + this.router.navigate(['collections', this.collectionTag.id], {replaceUrl: true, queryParamsHandling: 'merge', queryParams: {page: this.seriesPagination.currentPage} }); + } + + loadPage() { + const page = this.route.snapshot.queryParamMap.get('page'); + if (page != null) { + if (this.seriesPagination === undefined || this.seriesPagination === null) { + this.seriesPagination = {currentPage: 0, itemsPerPage: 30, totalItems: 0, totalPages: 1}; + } + this.seriesPagination.currentPage = parseInt(page, 10); + } + // Reload page after a series is updated or first load + this.seriesService.getSeriesForTag(this.collectionTag.id, this.seriesPagination?.currentPage, this.seriesPagination?.itemsPerPage).subscribe(tags => { + this.series = tags.result; + this.seriesPagination = tags.pagination; + this.isLoading = false; + window.scrollTo(0, 0); + }); + } + + updateFilter(data: UpdateFilterEvent) { + this.filter.mangaFormat = data.filterItem.value; + if (this.seriesPagination !== undefined && this.seriesPagination !== null) { + this.seriesPagination.currentPage = 1; + this.onPageChange(this.seriesPagination); + } else { + this.loadPage(); + } + } + + handleCollectionActionCallback(action: Action, collectionTag: CollectionTag) { + switch (action) { + case(Action.Edit): + this.openEditCollectionTagModal(this.collectionTag); + break; + default: + break; + } + } + + openEditCollectionTagModal(collectionTag: CollectionTag) { + const modalRef = this.modalService.open(EditCollectionTagsComponent, { size: 'lg', scrollable: true }); + modalRef.componentInstance.tag = this.collectionTag; + modalRef.closed.subscribe((results: {success: boolean, coverImageUpdated: boolean}) => { + this.loadPage(); + if (results.coverImageUpdated) { + this.tagImage = this.imageService.randomize(this.imageService.getCollectionCoverImage(collectionTag.id)); + } + }); + } + +} diff --git a/UI/Web/src/app/collections/collections-routing.module.ts b/UI/Web/src/app/collections/collections-routing.module.ts new file mode 100644 index 000000000..ad6249f8c --- /dev/null +++ b/UI/Web/src/app/collections/collections-routing.module.ts @@ -0,0 +1,24 @@ +import { NgModule } from '@angular/core'; +import { Routes, RouterModule } from '@angular/router'; +import { AuthGuard } from '../_guards/auth.guard'; +import { AllCollectionsComponent } from './all-collections/all-collections.component'; +import { CollectionDetailComponent } from './collection-detail/collection-detail.component'; + +const routes: Routes = [ + { + path: '', + runGuardsAndResolvers: 'always', + canActivate: [AuthGuard], + children: [ + {path: '', component: AllCollectionsComponent, pathMatch: 'full'}, + {path: ':id', component: CollectionDetailComponent}, + ] + } +]; + + +@NgModule({ + imports: [RouterModule.forChild(routes), ], + exports: [RouterModule] +}) +export class CollectionsRoutingModule { } diff --git a/UI/Web/src/app/collections/collections.module.ts b/UI/Web/src/app/collections/collections.module.ts new file mode 100644 index 000000000..eb0f8690e --- /dev/null +++ b/UI/Web/src/app/collections/collections.module.ts @@ -0,0 +1,23 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { CollectionDetailComponent } from './collection-detail/collection-detail.component'; +import { SharedModule } from '../shared/shared.module'; +import { CollectionsRoutingModule } from './collections-routing.module'; +import { CardsModule } from '../cards/cards.module'; +import { AllCollectionsComponent } from './all-collections/all-collections.component'; + + + +@NgModule({ + declarations: [ + AllCollectionsComponent, + CollectionDetailComponent + ], + imports: [ + CommonModule, + SharedModule, + CardsModule, + CollectionsRoutingModule, + ] +}) +export class CollectionsModule { } diff --git a/UI/Web/src/app/library/library.component.ts b/UI/Web/src/app/library/library.component.ts index 061e9e26d..e1d3a3982 100644 --- a/UI/Web/src/app/library/library.component.ts +++ b/UI/Web/src/app/library/library.component.ts @@ -120,11 +120,10 @@ export class LibraryComponent implements OnInit, OnDestroy { case(Action.Edit): const modalRef = this.modalService.open(EditCollectionTagsComponent, { size: 'lg', scrollable: true }); modalRef.componentInstance.tag = collectionTag; - modalRef.closed.subscribe((reloadNeeded: boolean) => { - if (reloadNeeded) { - // Reload tags - this.reloadTags(); - collectionTag.coverImage = this.imageService.randomize(this.imageService.getCollectionCoverImage(collectionTag.id)); + modalRef.closed.subscribe((results: {success: boolean, coverImageUpdated: boolean}) => { + this.reloadTags(); + if (results.coverImageUpdated) { + collectionTag.coverImage = this.imageService.randomize(collectionTag.coverImage); } }); break; diff --git a/UI/Web/src/app/series-detail/series-detail.component.scss b/UI/Web/src/app/series-detail/series-detail.component.scss index 8b3da7c30..06c2a718c 100644 --- a/UI/Web/src/app/series-detail/series-detail.component.scss +++ b/UI/Web/src/app/series-detail/series-detail.component.scss @@ -6,16 +6,16 @@ } -.poster { - width: 100%; - max-height: 481px; -} - .rating-star { margin-top: 2px; font-size: 1.5rem; } +.poster { + width: 100%; + max-height: 481px; +} + // Including breakpoint has issue with resovling variable, so copying gridpoints here @include media-breakpoint-down(xs, (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px)) {