Next Estimated Chapter (#2342)

This commit is contained in:
Joe Milazzo 2023-10-22 10:44:26 -05:00 committed by GitHub
parent ca5afe94d3
commit de9b09c71f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 433 additions and 73 deletions

View file

@ -0,0 +1,10 @@
export interface NextExpectedChapter {
volumeNumber: number;
chapterNumber: number;
expectedDate: string | null;
title: string;
/**
* Not real, used for some type stuff with app-card
*/
id: number;
}

View file

@ -17,6 +17,7 @@ export class ImageService {
public errorImage = 'assets/images/error-placeholder2.dark-min.png';
public resetCoverImage = 'assets/images/image-reset-cover-min.png';
public errorWebLinkImage = 'assets/images/broken-white-32x32.png';
public nextChapterImage = 'assets/images/image-placeholder.dark-min.png'
constructor(private accountService: AccountService, private themeService: ThemeService) {
this.themeService.currentTheme$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(theme => {

View file

@ -21,6 +21,7 @@ import {UserReview} from "../_single-module/review-card/user-review";
import {Rating} from "../_models/rating";
import {Recommendation} from "../_models/series-detail/recommendation";
import {ExternalSeriesDetail} from "../_models/series-detail/external-series-detail";
import {NextExpectedChapter} from "../_models/series-detail/next-expected-chapter";
@Injectable({
providedIn: 'root'
@ -233,4 +234,8 @@ export class SeriesService {
return this.httpClient.get<ExternalSeriesDetail>(this.baseUrl + 'series/external-series-detail?aniListId=' + (aniListId || 0) + '&malId=' + (malId || 0) + '&seriesId=' + (seriesId || 0));
}
getNextExpectedChapterDate(seriesId: number) {
return this.httpClient.get<NextExpectedChapter>(this.baseUrl + 'series/next-expected?seriesId=' + seriesId);
}
}

View file

@ -56,6 +56,17 @@ export class ManageTasksSettingsComponent implements OnInit {
api: this.serverService.bustCache(),
successMessage: 'bust-cache-task-success'
},
{
name: 'bust-locale-task',
description: 'bust-locale-task-desc',
api: defer(() => {
localStorage.removeItem('@transloco/translations/timestamp');
localStorage.removeItem('@transloco/translations');
localStorage.removeItem('translocoLang');
return of();
}),
successMessage: 'bust-locale-task-success',
},
{
name: 'clear-reading-cache-task',
description: 'clear-reading-cache-task-desc',

View file

@ -324,6 +324,7 @@ $action-bar-height: 38px;
color: $primary-color;
}
$pagination-color: transparent;
.right {
@ -332,7 +333,7 @@ $action-bar-height: 38px;
top: $action-bar-height;
width: 20vw;
z-index: 3;
background: transparent;
background: $pagination-color;
border-color: transparent;
border: none !important;
opacity: 0;
@ -354,7 +355,7 @@ $action-bar-height: 38px;
top: $action-bar-height;
width: 18%;
z-index: 3;
background: transparent;
background: $pagination-color;
border-color: transparent;
border: none !important;
opacity: 0;
@ -374,7 +375,7 @@ $action-bar-height: 38px;
left: 0px;
top: $action-bar-height;
width: 20vw;
background: transparent;
background: $pagination-color;
border-color: transparent;
border: none !important;
z-index: 3;

View file

@ -34,7 +34,7 @@
<span class="badge bg-primary">{{count}}</span>
</div>
<div class="card-overlay"></div>
<div class="overlay-information" *ngIf="overlayInformation !== '' || overlayInformation !== undefined">
<div class="overlay-information {{centerOverlay ? 'overlay-information--centered' : ''}}" *ngIf="overlayInformation !== '' || overlayInformation !== undefined">
<div class="position-relative">
<span class="card-title library mx-auto" style="width: auto;" [ngbTooltip]="overlayInformation" placement="top">{{overlayInformation}}</span>
</div>

View file

@ -77,7 +77,7 @@ $image-width: 160px;
position: absolute;
top: 0;
width: 158px;
}
.not-read-badge {
@ -111,6 +111,8 @@ $image-width: 160px;
}
}
.overlay-information {
position: absolute;
top: 5px;
@ -118,13 +120,18 @@ $image-width: 160px;
border-radius: 15px;
padding: 0 10px;
background-color: var(--card-bg-color);
&.overlay-information--centered {
top: 95px;
left: 36px;
}
}
.overlay {
height: $image-height;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
&:hover {
visibility: visible;
@ -138,11 +145,11 @@ $image-width: 160px;
z-index: 100;
}
}
.overlay-item {
visibility: hidden;
}
z-index: 10;
.count {

View file

@ -39,8 +39,10 @@ import {MangaFormatIconPipe} from "../../pipe/manga-format-icon.pipe";
import {SentenceCasePipe} from "../../pipe/sentence-case.pipe";
import {CommonModule} from "@angular/common";
import {RouterLink} from "@angular/router";
import {TranslocoModule} from "@ngneat/transloco";
import {translate, TranslocoModule} from "@ngneat/transloco";
import {CardActionablesComponent} from "../../_single-module/card-actionables/card-actionables.component";
import {NextExpectedChapter} from "../../_models/series-detail/next-expected-chapter";
import {UtcToLocalTimePipe} from "../../pipe/utc-to-local-time.pipe";
@Component({
selector: 'app-card-item',
@ -96,7 +98,7 @@ export class CardItemComponent implements OnInit {
/**
* This is the entity we are representing. It will be returned if an action is executed.
*/
@Input({required: true}) entity!: Series | Volume | Chapter | CollectionTag | PageBookmark | RecentlyAddedItem;
@Input({required: true}) entity!: Series | Volume | Chapter | CollectionTag | PageBookmark | RecentlyAddedItem | NextExpectedChapter;
/**
* If the entity is selected or not.
*/
@ -117,6 +119,10 @@ export class CardItemComponent implements OnInit {
* Additional information to show on the overlay area. Will always render.
*/
@Input() overlayInformation: string = '';
/**
* If overlay is enabled, should the text be centered or not
*/
@Input() centerOverlay = false;
/**
* Event emitted when item is clicked
*/
@ -210,8 +216,29 @@ export class CardItemComponent implements OnInit {
}
} else if (this.utilityService.isSeries(this.entity)) {
this.tooltipTitle = this.title || (this.utilityService.asSeries(this.entity).name);
} else if (this.entity.hasOwnProperty('expectedDate')) {
this.suppressArchiveWarning = true;
this.imageUrl = '';
const nextDate = (this.entity as NextExpectedChapter);
// if (nextDate.volumeNumber > 0 && nextDate.chapterNumber === 0) {
// this.overlayInformation = 'Volume ' + nextDate.volumeNumber;
//
// } else {
// this.overlayInformation = 'Chapter ' + nextDate.chapterNumber;
// }
this.overlayInformation = nextDate.title;
this.centerOverlay = true;
if (nextDate.expectedDate) {
const utcPipe = new UtcToLocalTimePipe();
this.title = utcPipe.transform(nextDate.expectedDate);
}
this.cdRef.markForCheck();
}
this.filterSendTo();
this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(user => {
this.user = user;

View file

@ -3,7 +3,7 @@
}
.book-title {
margin: 8px 0 4px !important;
margin: 8px 0 4px !important;
}
// Override since it's not coming from library
@ -11,10 +11,15 @@
margin: 3px 0 4px !important;
}
::ng-deep .dark > ngx-extended-pdf-viewer .treeItem>a {
color: lightblue !important;
}
.progress-container {
width: 100%;
}
.progress-bar {
// NOTE: We have to override due to theme variables not being available
background-color: #3B9E76;
}
}

View file

@ -157,6 +157,7 @@
[selected]="bulkSelectionService.isCardSelected('chapter', scroll.viewPortInfo.startIndexWithBuffer + idx)" [allowSelection]="true"></app-card-item>
</ng-template>
</ng-container>
<ng-container [ngTemplateOutlet]="estimatedNextCard" [ngTemplateOutletContext]="{tabId: TabID.Storyline}"></ng-container>
</div>
</ng-container>
<ng-template #storylineListLayout>
@ -203,6 +204,7 @@
[selected]="bulkSelectionService.isCardSelected('volume', scroll.viewPortInfo.startIndexWithBuffer + idx)" [allowSelection]="true">
</app-card-item>
</ng-container>
<ng-container [ngTemplateOutlet]="estimatedNextCard" [ngTemplateOutletContext]="{tabId: TabID.Volumes}"></ng-container>
</div>
</ng-container>
<ng-template #volumeListLayout>
@ -240,6 +242,7 @@
</ng-container>
</app-card-item>
</div>
<ng-container [ngTemplateOutlet]="estimatedNextCard" [ngTemplateOutletContext]="{tabId: TabID.Chapters}"></ng-container>
</div>
</ng-container>
<ng-template #chapterListLayout>
@ -354,5 +357,26 @@
<app-loading [loading]="isLoading"></app-loading>
</div>
<ng-template #estimatedNextCard let-tabId="tabId">
<ng-container *ngIf="nextExpectedChapter">
<ng-container [ngSwitch]="tabId">
<ng-container *ngSwitchCase="TabID.Volumes">
<app-card-item *ngIf="nextExpectedChapter.volumeNumber > 0 && nextExpectedChapter.chapterNumber === 0" class="col-auto mt-2 mb-2" [entity]="nextExpectedChapter" ></app-card-item>
</ng-container>
<ng-container *ngSwitchCase="TabID.Chapters">
<app-card-item class="col-auto mt-2 mb-2" [entity]="nextExpectedChapter" ></app-card-item>
</ng-container>
<ng-container *ngSwitchCase="TabID.Storyline">
<app-card-item class="col-auto mt-2 mb-2" [entity]="nextExpectedChapter" ></app-card-item>
</ng-container>
</ng-container>
<!-- <ng-container *ngIf="tabId === TabID.Volumes && nextExpectedChapter.volumeNumber > 0 && nextExpectedChapter.chapterNumber === 0">-->
<!-- <app-card-item class="col-auto mt-2 mb-2" [entity]="nextExpectedChapter" ></app-card-item>-->
<!-- </ng-container>-->
<!-- <ng-container *ngIf="tabId === TabID.Storyline || tabId === TabID.Chapters && nextExpectedChapter.chapterNumber !== 0">-->
<!-- <app-card-item class="col-auto mt-2 mb-2" [entity]="nextExpectedChapter" ></app-card-item>-->
<!-- </ng-container>-->
</ng-container>
</ng-template>
</ng-container>

View file

@ -1,4 +1,4 @@
import { DOCUMENT, NgIf, NgStyle, NgFor, DecimalPipe } from '@angular/common';
import {DecimalPipe, DOCUMENT, NgFor, NgIf, NgStyle, NgSwitch, NgSwitchCase, NgTemplateOutlet} from '@angular/common';
import {
AfterContentChecked,
ChangeDetectionStrategy,
@ -12,17 +12,31 @@ import {
OnInit,
ViewChild
} from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import {FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms';
import {Title} from '@angular/platform-browser';
import {ActivatedRoute, Router} from '@angular/router';
import { NgbModal, NgbNavChangeEvent, NgbOffcanvas, NgbTooltip, NgbProgressbar, NgbDropdown, NgbDropdownToggle, NgbDropdownMenu, NgbDropdownItem, NgbNav, NgbNavItem, NgbNavLink, NgbNavContent, NgbNavOutlet } from '@ng-bootstrap/ng-bootstrap';
import {
NgbDropdown,
NgbDropdownItem,
NgbDropdownMenu,
NgbDropdownToggle,
NgbModal,
NgbNav,
NgbNavChangeEvent,
NgbNavContent,
NgbNavItem,
NgbNavLink,
NgbNavOutlet,
NgbOffcanvas,
NgbProgressbar,
NgbTooltip
} from '@ng-bootstrap/ng-bootstrap';
import {ToastrService} from 'ngx-toastr';
import {catchError, forkJoin, of} from 'rxjs';
import {take} from 'rxjs/operators';
import {BulkSelectionService} from 'src/app/cards/bulk-selection.service';
import {CardDetailDrawerComponent} from 'src/app/cards/card-detail-drawer/card-detail-drawer.component';
import {EditSeriesModalComponent} from 'src/app/cards/_modals/edit-series-modal/edit-series-modal.component';
import {ConfirmService} from 'src/app/shared/confirm.service';
import {TagBadgeCursor} from 'src/app/shared/tag-badge/tag-badge.component';
import {DownloadService} from 'src/app/shared/_services/download.service';
import {KEY_CODES, UtilityService} from 'src/app/shared/_services/utility.service';
@ -54,27 +68,30 @@ import {ReviewSeriesModalComponent} from '../../../_single-module/review-series-
import {PageLayoutMode} from 'src/app/_models/page-layout-mode';
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import {UserReview} from "../../../_single-module/review-card/user-review";
import { LoadingComponent } from '../../../shared/loading/loading.component';
import { ExternalListItemComponent } from '../../../cards/external-list-item/external-list-item.component';
import { ExternalSeriesCardComponent } from '../../../cards/external-series-card/external-series-card.component';
import { SeriesCardComponent } from '../../../cards/series-card/series-card.component';
import { EntityTitleComponent } from '../../../cards/entity-title/entity-title.component';
import { ListItemComponent } from '../../../cards/list-item/list-item.component';
import { CardItemComponent } from '../../../cards/card-item/card-item.component';
import { VirtualScrollerModule } from '@iharbeck/ngx-virtual-scroller';
import { BulkOperationsComponent } from '../../../cards/bulk-operations/bulk-operations.component';
import { ReviewCardComponent } from '../../../_single-module/review-card/review-card.component';
import { CarouselReelComponent } from '../../../carousel/_components/carousel-reel/carousel-reel.component';
import { SeriesMetadataDetailComponent } from '../series-metadata-detail/series-metadata-detail.component';
import { ImageComponent } from '../../../shared/image/image.component';
import { TagBadgeComponent } from '../../../shared/tag-badge/tag-badge.component';
import { SideNavCompanionBarComponent } from '../../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component';
import {LoadingComponent} from '../../../shared/loading/loading.component';
import {ExternalListItemComponent} from '../../../cards/external-list-item/external-list-item.component';
import {ExternalSeriesCardComponent} from '../../../cards/external-series-card/external-series-card.component';
import {SeriesCardComponent} from '../../../cards/series-card/series-card.component';
import {EntityTitleComponent} from '../../../cards/entity-title/entity-title.component';
import {ListItemComponent} from '../../../cards/list-item/list-item.component';
import {CardItemComponent} from '../../../cards/card-item/card-item.component';
import {VirtualScrollerModule} from '@iharbeck/ngx-virtual-scroller';
import {BulkOperationsComponent} from '../../../cards/bulk-operations/bulk-operations.component';
import {ReviewCardComponent} from '../../../_single-module/review-card/review-card.component';
import {CarouselReelComponent} from '../../../carousel/_components/carousel-reel/carousel-reel.component';
import {SeriesMetadataDetailComponent} from '../series-metadata-detail/series-metadata-detail.component';
import {ImageComponent} from '../../../shared/image/image.component';
import {TagBadgeComponent} from '../../../shared/tag-badge/tag-badge.component';
import {
SideNavCompanionBarComponent
} from '../../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component';
import {TranslocoDirective, TranslocoService} from "@ngneat/transloco";
import {CardActionablesComponent} from "../../../_single-module/card-actionables/card-actionables.component";
import {ExternalSeries} from "../../../_models/series-detail/external-series";
import {
SeriesPreviewDrawerComponent
} from "../../../_single-module/series-preview-drawer/series-preview-drawer.component";
import {PublicationStatus} from "../../../_models/metadata/publication-status";
interface RelatedSeriesPair {
series: Series;
@ -102,7 +119,7 @@ interface StoryLineItem {
styleUrls: ['./series-detail.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [NgIf, SideNavCompanionBarComponent, CardActionablesComponent, ReactiveFormsModule, NgStyle, TagBadgeComponent, ImageComponent, NgbTooltip, NgbProgressbar, NgbDropdown, NgbDropdownToggle, NgbDropdownMenu, NgbDropdownItem, SeriesMetadataDetailComponent, CarouselReelComponent, ReviewCardComponent, BulkOperationsComponent, NgbNav, NgbNavItem, NgbNavLink, NgbNavContent, VirtualScrollerModule, NgFor, CardItemComponent, ListItemComponent, EntityTitleComponent, SeriesCardComponent, ExternalSeriesCardComponent, ExternalListItemComponent, NgbNavOutlet, LoadingComponent, DecimalPipe, TranslocoDirective]
imports: [NgIf, SideNavCompanionBarComponent, CardActionablesComponent, ReactiveFormsModule, NgStyle, TagBadgeComponent, ImageComponent, NgbTooltip, NgbProgressbar, NgbDropdown, NgbDropdownToggle, NgbDropdownMenu, NgbDropdownItem, SeriesMetadataDetailComponent, CarouselReelComponent, ReviewCardComponent, BulkOperationsComponent, NgbNav, NgbNavItem, NgbNavLink, NgbNavContent, VirtualScrollerModule, NgFor, CardItemComponent, ListItemComponent, EntityTitleComponent, SeriesCardComponent, ExternalSeriesCardComponent, ExternalListItemComponent, NgbNavOutlet, LoadingComponent, DecimalPipe, TranslocoDirective, NgTemplateOutlet, NgSwitch, NgSwitchCase]
})
export class SeriesDetailComponent implements OnInit, AfterContentChecked {
@ -150,6 +167,8 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
seriesImage: string = '';
downloadInProgress: boolean = false;
nextExpectedChapter: any | undefined;
/**
* Track by function for Volume to tell when to refresh card data
*/
@ -293,7 +312,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
public utilityService: UtilityService, private toastr: ToastrService,
private accountService: AccountService, public imageService: ImageService,
private actionFactoryService: ActionFactoryService, private libraryService: LibraryService,
private confirmService: ConfirmService, private titleService: Title,
private titleService: Title,
private downloadService: DownloadService, private actionService: ActionService,
private messageHub: MessageHubService, private readingListService: ReadingListService,
public navService: NavService,
@ -501,6 +520,14 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
this.seriesService.getMetadata(seriesId).subscribe(metadata => {
this.seriesMetadata = metadata;
this.cdRef.markForCheck();
if (![PublicationStatus.Ended, PublicationStatus.OnGoing].includes(this.seriesMetadata.publicationStatus)) return;
this.seriesService.getNextExpectedChapterDate(seriesId).subscribe(date => {
if (date == null || date.expectedDate === null) return;
this.nextExpectedChapter = date;
this.cdRef.markForCheck();
})
});
this.seriesService.isWantToRead(seriesId).subscribe(isWantToRead => {

View file

@ -64,7 +64,7 @@ export class ChangeEmailComponent implements OnInit {
if (updateEmailResponse.hadNoExistingEmail) {
this.toastr.success(translate('toasts.email-sent-to-no-existing', {email: model.email}));
} else {
this.toastr.success(translate('toasts.email-send-to'));
this.toastr.success(translate('toasts.email-sent-to'));
}
} else {
this.toastr.success(translate('toasts.change-email-private'));

View file

@ -897,7 +897,6 @@
"card-item": {
"cannot-read": "Cannot Read"
},
"chapter-metadata-detail": {
@ -1191,6 +1190,10 @@
"bust-cache-task-desc": "Busts the Kavita+ Cache - should only be used when debugging bad matches.",
"bust-cache-task-success": "Kavita+ Cache busted",
"bust-locale-task": "Bust Locale Cache",
"bust-locale-task-desc": "Busts the Locale Cache. This can fix issues with strings not showing correctly after an update",
"bust-locale-task-success": "Locale Cache busted",
"clear-reading-cache-task": "Clear Reading Cache",
"clear-reading-cache-task-desc": "Clears cached files for reading. Useful when you've just updated a file that you were previously reading within the last 24 hours.",
"clear-reading-cache-task-success": "Cache has been cleared",