mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Collection Redesign (#500)
* Setup UI for the collection redesign. * Implemented collection details page
This commit is contained in:
parent
226d7408bf
commit
51ea41fc35
@ -20,7 +20,7 @@ namespace Kavita.Common.EnvironmentInfo
|
||||
var config = attributes.OfType<AssemblyConfigurationAttribute>().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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ const routes: Routes = [
|
||||
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes), ],
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AdminRoutingModule { }
|
||||
|
@ -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'}
|
||||
];
|
||||
|
@ -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',
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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;
|
@ -0,0 +1,56 @@
|
||||
<div class="container-fluid" *ngIf="collectionTag !== undefined" style="padding-top: 10px">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-2 col-xs-4 col-sm-6">
|
||||
<img class="poster lazyload" [src]="imageService.placeholderImage" [attr.data-src]="tagImage"
|
||||
(error)="imageService.updateErroredImage($event)" aria-hidden="true">
|
||||
</div>
|
||||
<div class="col-md-10 col-xs-8 col-sm-6">
|
||||
<div class="row no-gutters">
|
||||
<h2>
|
||||
{{collectionTag.title}}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="row no-gutters mt-2 mb-2">
|
||||
<!-- <div>
|
||||
<button class="btn btn-primary" (click)="read()" [disabled]="isLoading">
|
||||
<span>
|
||||
<i class="fa fa-book-open"></i>
|
||||
</span>
|
||||
<span class="read-btn--text"> Read</span>
|
||||
</button>
|
||||
</div> -->
|
||||
<div class="ml-2" *ngIf="isAdmin">
|
||||
<button class="btn btn-secondary" (click)="openEditCollectionTagModal(collectionTag)" title="Edit Series information">
|
||||
<span>
|
||||
<i class="fa fa-pen" aria-hidden="true"></i>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row no-gutters">
|
||||
<app-read-more [text]="collectionTag.summary" [maxLength]="250"></app-read-more>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<app-card-detail-layout
|
||||
header="Series"
|
||||
[isLoading]="isLoading"
|
||||
[items]="series"
|
||||
[pagination]="seriesPagination"
|
||||
(pageChange)="onPageChange($event)"
|
||||
[filters]="filters"
|
||||
(applyFilter)="updateFilter($event)"
|
||||
>
|
||||
<ng-template #cardItem let-item let-position="idx">
|
||||
<app-series-card [data]="item" [libraryId]="item.libraryId" (reload)="loadPage()"></app-series-card>
|
||||
</ng-template>
|
||||
</app-card-detail-layout>
|
||||
|
||||
<div class="mx-auto" *ngIf="isLoading" style="width: 200px;">
|
||||
<div class="spinner-border text-secondary loading" role="status">
|
||||
<span class="invisible">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -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;
|
||||
}
|
||||
}
|
@ -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<Series> = [];
|
||||
seriesPagination!: Pagination;
|
||||
collectionTagActions: ActionItem<CollectionTag>[] = [];
|
||||
isAdmin: boolean = false;
|
||||
filters: Array<FilterItem> = 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));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
24
UI/Web/src/app/collections/collections-routing.module.ts
Normal file
24
UI/Web/src/app/collections/collections-routing.module.ts
Normal file
@ -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 { }
|
23
UI/Web/src/app/collections/collections.module.ts
Normal file
23
UI/Web/src/app/collections/collections.module.ts
Normal file
@ -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 { }
|
@ -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;
|
||||
|
@ -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)) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user