This commit is contained in:
Christopher 2025-06-29 10:32:59 +02:00 committed by GitHub
commit 4c18820e5f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 466 additions and 359 deletions

View file

@ -1,171 +1,211 @@
@use './theme/variables' as theme; @use './theme/variables' as theme;
.title {
color: white;
font-weight: bold;
font-size: 1.75rem;
}
.image-container {
align-self: flex-start;
max-height: 400px;
max-width: 280px;
}
.subtitle {
color: var(--detail-subtitle-color);
font-weight: bold;
font-size: 0.8rem;
}
.main-container { .main-container {
overflow: unset !important; overflow: unset !important;
margin-top: 15px; margin-top: 0.9375rem;
}
::ng-deep .badge-expander .content a { .info-container {
font-size: 0.8rem; .info-grid-container {
} display: grid;
grid-template-columns: 20% 80%;
.btn-group > .btn.dropdown-toggle-split:not(first-child){ grid-template-rows: auto auto;
border-top-right-radius: var(--bs-border-radius) !important; .image-container {
border-bottom-right-radius: var(--bs-border-radius) !important; grid-column: 1;
border-width: 1px 1px 1px 0 !important; grid-row: 1 / 3;
} margin-right: 1.5rem;
.btn-group > .btn:not(:last-child):not(.dropdown-toggle) {
border-width: 1px 0 1px 1px !important;
}
.card-body > div:nth-child(2) {
height: 50px;
overflow: hidden;
-webkit-line-clamp: 2;
display: -webkit-box;
overflow: hidden;
-webkit-box-orient: vertical;
}
.under-image ~ .overlay-information {
top: -404px;
height: 364px;
}
.overlay-information {
position: relative;
top: -364px;
height: 364px;
transition: all 0.2s;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
&:hover {
cursor: pointer;
background-color: var(--card-overlay-hover-bg-color) !important;
.overlay-information--centered {
visibility: visible;
} }
} .metadata-details-upper {
display: flex;
.overlay-information--centered { flex-direction: column;
position: absolute; justify-content: center;
border-radius: 15px; grid-column: 2;
background-color: rgba(0, 0, 0, .7); grid-row: 1;
border-radius: 50px; .title {
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 115;
visibility: hidden;
&:hover {
background-color: var(--primary-color) !important;
cursor: pointer;
}
div {
width: 60px;
height: 60px;
i {
font-size: 1.6rem;
line-height: 60px;
width: 100%;
}
}
}
}
.progress {
border-radius: 0;
}
.progress-banner.series {
position: relative;
}
::ng-deep .progress-banner.series span {
position: absolute;
left: 50%;
transform: translate(-50%, -50%);
color: white; color: white;
top: 50%; font-weight: bold;
} font-size: 1.75rem;
}
.subtitle {
color: var(--detail-subtitle-color);;
font-weight: bold;
font-size: 0.8rem;
}
}
.metadata-details-lower {
grid-column: 2;
grid-row: 2;
.continue-container {
display: flex;
.carousel-tabs-container { .actions-row {
display: flex;
width: unset;
}
}
.upper-details {
font-size: 0.9rem;
}
}
}
}
.carousel-tabs-container {
overflow-x: auto; overflow-x: auto;
white-space: nowrap; white-space: nowrap;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
-ms-overflow-style: -ms-autohiding-scrollbar; -ms-overflow-style: -ms-autohiding-scrollbar;
scrollbar-width: none; scrollbar-width: none;
box-shadow: inset -1px -2px 0px -1px var(--elevation-layer9); box-shadow: inset -0.0625rem -0.125rem 0.0rem -0.0625rem var(--elevation-layer9);
.nav-tabs {
flex-wrap: nowrap;
}
}
} }
.carousel-tabs-container::-webkit-scrollbar { .carousel-tabs-container::-webkit-scrollbar {
display: none; display: none;
} }
.nav-tabs {
flex-wrap: nowrap;
}
.upper-details { :host ::ng-deep {
font-size: 0.9rem; .main-container {
} .info-container {
.info-grid-container {
::ng-deep .carousel-container .header i.fa-plus, ::ng-deep .carousel-container .header i.fa-pen{ .image-container {
border-width: 1px; app-image {
border-style: solid; img {
border-radius: 5px;
border-color: var(--primary-color);
padding: 5px;
vertical-align: middle;
&:hover {
background-color: var(--primary-color-dark-shade);
}
}
::ng-deep .image-container.mobile-bg app-image img {
max-height: 400px;
object-fit: contain;
}
@media (max-width: theme.$grid-breakpoints-lg) {
.carousel-tabs-container {
mask-image: linear-gradient(transparent, black 0%, black 90%, transparent 100%);
-webkit-mask-image: linear-gradient(to right, transparent, black 0%, black 90%, transparent 100%);
}
}
::ng-deep .image-container.mobile-bg app-image img {
max-height: 100dvh !important; max-height: 100dvh !important;
object-fit: cover !important; object-fit: cover !important;
}
}
.progress-banner {
display: block;
}
.under-image {
display: block;
}
.overlay-information {
left: 0;
width: 100%;
}
}
.metadata-details-lower {
.continue-container {
.btn-group {
>.btn {
border: unset;
background-color: var(--primary-color-dark-shade);
&:hover {
border: unset;
background-color: var(--primary-color-darker-shade);
}
span {
i {
padding-right: 0.2rem;
}
}
}
.dropdown {
.dropdown-toggle-split {
border-top-right-radius: var(--bs-border-radius);
border-bottom-right-radius: var(--bs-border-radius);
background-color: var(--primary-color-dark-shade);
border: unset;
box-shadow: inset 1px 0 1px 0 rgba(0,0,0,0.1);
&:hover {
background-color: var(--primary-color-darker-shade);
}
&::after {
vertical-align: 0.11rem;
}
}
}
}
}
}
}
}
}
} }
/* col-lg */
@media (max-width: theme.$grid-breakpoints-lg) { @media (max-width: theme.$grid-breakpoints-lg) {
.image-container.mobile-bg{ .main-container {
.info-container {
.info-grid-container {
grid-template-columns: 30% 70%;
overflow: hidden;
padding-bottom: 1.5rem;
}
}
.carousel-tabs-container {
mask-image: linear-gradient(to right, transparent, black 0%, black 90%, transparent 100%);
-webkit-mask-image: linear-gradient(to right, transparent, black 0%, black 90%, transparent 100%);
}
}
}
@media (max-width: 760px) {
.main-container {
.info-container {
.info-grid-container {
grid-template-columns: 35% 65%;
}
}
}
}
@media (max-width: theme.$grid-breakpoints-sm) {
.main-container {
.info-container {
.info-grid-container {
grid-template-columns: 35% 65%;
grid-template-rows: auto auto;
padding-bottom: unset;
.image-container {
display: block !important;
grid-column: 1;
grid-row: 1;
margin-right: 1rem;
}
.metadata-details-upper {
display: flex;
flex-direction: column;
justify-content: center;
grid-column: 2;
grid-row: 1;
.title {
font-size: 1.375rem;
}
.subtitle {
font-size: 0.625rem;
}
}
.metadata-details-lower {
grid-column: 1 / 3;
grid-row: 2;
margin-top: 2rem;
}
}
}
}
:host ::ng-deep {
.read-more-cont {
div {
max-width: unset !important;
}
}
}
}
@media (max-width: theme.$grid-breakpoints-smm) {
.main-container {
.info-container {
.info-grid-container {
.image-container.mobile-background {
width: 100vw; width: 100vw;
top: calc(var(--nav-offset) - 20px); top: calc(var(--nav-offset) - 1.25rem);
left: 0; left: 0;
pointer-events: none; pointer-events: none;
position: fixed !important; position: fixed !important;
@ -174,11 +214,31 @@
max-width: unset !important; max-width: unset !important;
height: 100dvh !important; height: 100dvh !important;
} }
.metadata-details-upper {
::ng-deep .image-container.mobile-bg app-image img { grid-column: 1 / 3;
}
.metadata-details-lower {
margin-top: 0rem;
.continue-container {
flex-direction: column;
.actions-row {
margin-top: 0.5rem;
}
}
}
}
}
}
:host ::ng-deep {
.main-container {
.info-container {
.info-grid-container {
.image-container.mobile-background {
app-image {
img {
max-height: unset !important; max-height: unset !important;
opacity: 0.05 !important; opacity: 0.05 !important;
filter: blur(5px) !important; filter: blur(0.3125rem) !important;
max-width: 100dvw; max-width: 100dvw;
height: 100dvh !important; height: 100dvh !important;
overflow: hidden; overflow: hidden;
@ -187,23 +247,27 @@
left: 0; left: 0;
object-fit: cover; object-fit: cover;
} }
}
.progress-banner { }
display:none; }
}
}
} }
:host ::ng-deep {
.main-container {
.info-container {
.info-grid-container {
.image-container {
.progress-banner {
display: none;
}
.under-image { .under-image {
display: none; display: none;
} }
}
} }
.upper-details { }
font-size: 0.9rem; }
}
@media (max-width: theme.$grid-breakpoints-lg) {
.carousel-tabs-container {
mask-image: linear-gradient(to right, transparent, black 0%, black 90%, transparent 100%);
-webkit-mask-image: linear-gradient(to right, transparent, black 0%, black 90%, transparent 100%);
} }
} }

View file

@ -13,8 +13,10 @@
.time-left{ .time-left{
font-size: 0.8rem; font-size: 0.8rem;
display: inline-block;
} }
.word-count { .word-count {
font-size: 0.8rem; font-size: 0.8rem;
display: inline-block;
} }

View file

@ -4,26 +4,29 @@
@if (series && seriesMetadata && libraryType !== null) { @if (series && seriesMetadata && libraryType !== null) {
<div [ngStyle]="{'height': ScrollingBlockHeight}" class="main-container container-fluid" #scrollingBlock> <div [ngStyle]="{'height': ScrollingBlockHeight}" class="main-container container-fluid" #scrollingBlock>
<div class="row mb-0 mb-xl-3 info-container"> <div class="row mb-0 mb-xl-3 info-container">
<div [ngClass]="mobileSeriesImgBackground === 'true' ? 'mobile-bg' : ''"
class="image-container series col-5 col-sm-6 col-md-5 col-lg-5 col-xl-2 col-xxl-2 col-xxxl-2 d-none d-sm-block mb-3 position-relative"> <div class="info-grid-container">
<app-cover-image [entity]="series" [coverImage]="imageService.getSeriesCoverImage(series.id)" [continueTitle]="ContinuePointTitle" (read)="read()"></app-cover-image> <div [ngClass]="mobileSeriesImgBackground === 'true' ? 'mobile-background' : ''"
class="image-container series position-relative">
<app-cover-image [entity]="series" [coverImage]="imageService.getSeriesCoverImage(series.id)"
[continueTitle]="ContinuePointTitle" (read)="read()"></app-cover-image>
</div> </div>
<div class="col-xl-10 col-lg-7 col-md-12 col-sm-12 col-xs-12"> <div class="metadata-details-upper">
<div class="metadata-details-wrapper">
<h4 class="title mb-2"> <h4 class="title mb-2">
<span>{{series.name}} <span>{{ series.name }}
@if(isLoadingExtra || isLoading) { @if (isLoadingExtra || isLoading) {
<div class="spinner-border spinner-border-sm text-primary" role="status"> <div class="spinner-border spinner-border-sm text-primary" role="status">
<span class="visually-hidden">loading...</span> <span class="visually-hidden">loading...</span>
</div> </div>
} }
</span> </span>
</h4> </h4>
<div class="subtitle mt-2 mb-2"> <div class="subtitle mt-2 mb-2">
@if (series.localizedName !== series.name && series.localizedName) { @if (series.localizedName !== series.name && series.localizedName) {
<span>{{series.localizedName | defaultValue}}</span> <span>{{ series.localizedName | defaultValue }}</span>
} }
</div> </div>
@ -45,24 +48,28 @@
[webLinks]="WebLinks"> [webLinks]="WebLinks">
</app-external-rating> </app-external-rating>
</div> </div>
</div>
</div>
<div class="mt-3 mb-3"> <div class="metadata-details-lower">
<div class="row g-0" style="align-items: center;"> <div class="continue-container row g-0" style="align-items: center;">
<div class="col-auto"> <div class="col-auto">
<div class="btn-group"> <div class="btn-group">
<button type="button" class="btn btn-outline-primary" (click)="read()"> <button type="button" class="btn btn-outline-primary" (click)="read()">
<span> <span>
<i class="fa {{hasReadingProgress ? 'fa-book-open' : 'fa-book'}}" aria-hidden="true"></i> <i class="fa {{hasReadingProgress ? 'fa-book-open' : 'fa-book'}}" aria-hidden="true"></i>
<span class="read-btn--text">&nbsp;{{(hasReadingProgress) ? t('continue') : t('read')}}</span> <span class="read-btn--text">&nbsp;{{ (hasReadingProgress) ? t('continue') : t('read') }}</span>
</span> </span>
</button> </button>
<div class="btn-group" ngbDropdown role="group" display="dynamic" [attr.aria-label]="t('read-options-alt')"> <div class="btn-group" ngbDropdown role="group" display="dynamic"
<button type="button" class="btn btn-outline-primary dropdown-toggle-split" ngbDropdownToggle></button> [attr.aria-label]="t('read-options-alt')">
<button type="button" class="btn btn-outline-primary dropdown-toggle-split"
ngbDropdownToggle></button>
<div class="dropdown-menu" ngbDropdownMenu> <div class="dropdown-menu" ngbDropdownMenu>
<button ngbDropdownItem (click)="read(true)"> <button ngbDropdownItem (click)="read(true)">
<span> <span>
<i class="fa fa-glasses" aria-hidden="true"></i> <i class="fa fa-glasses" aria-hidden="true"></i>
<span class="read-btn--text">&nbsp;{{(hasReadingProgress) ? t('continue-incognito') : t('read-incognito')}}</span> <span class="read-btn--text">&nbsp;{{ (hasReadingProgress) ? t('continue-incognito') : t('read-incognito') }}</span>
</span> </span>
</button> </button>
</div> </div>
@ -70,8 +77,10 @@
</div> </div>
</div> </div>
<div class="actions-row">
<div class="col-auto ms-2"> <div class="col-auto ms-2">
<button class="btn btn-actions" (click)="toggleWantToRead()" ngbTooltip="{{isWantToRead ? t('remove-from-want-to-read') : t('add-to-want-to-read')}}"> <button class="btn btn-actions" (click)="toggleWantToRead()"
ngbTooltip="{{isWantToRead ? t('remove-from-want-to-read') : t('add-to-want-to-read')}}">
<span> <span>
<i class="{{isWantToRead ? 'fa-solid' : 'fa-regular'}} fa-star" aria-hidden="true"></i> <i class="{{isWantToRead ? 'fa-solid' : 'fa-regular'}} fa-star" aria-hidden="true"></i>
</span> </span>
@ -80,7 +89,8 @@
@if (isAdmin) { @if (isAdmin) {
<div class="col-auto ms-2"> <div class="col-auto ms-2">
<button class="btn btn-actions" id="edit-btn--komf" (click)="openEditSeriesModal()" [ngbTooltip]="t('edit-series-alt')"> <button class="btn btn-actions" id="edit-btn--komf" (click)="openEditSeriesModal()"
[ngbTooltip]="t('edit-series-alt')">
<span><i class="fa fa-pen" aria-hidden="true"></i></span> <span><i class="fa fa-pen" aria-hidden="true"></i></span>
</button> </button>
</div> </div>
@ -88,52 +98,58 @@
@if ((licenseService.hasValidLicense$ | async) && libraryAllowsScrobbling) { @if ((licenseService.hasValidLicense$ | async) && libraryAllowsScrobbling) {
<div class="col-auto ms-2"> <div class="col-auto ms-2">
<button class="btn btn-actions" (click)="toggleScrobbling($event)" [ngbTooltip]="t('scrobbling-tooltip', {value: isScrobbling ? t('on') : t('off')})"> <button class="btn btn-actions" (click)="toggleScrobbling($event)"
<i class="fa-solid fa-tower-{{(isScrobbling) ? 'broadcast' : 'observation'}}" aria-hidden="true"></i> [ngbTooltip]="t('scrobbling-tooltip', {value: isScrobbling ? t('on') : t('off')})">
<i class="fa-solid fa-tower-{{(isScrobbling) ? 'broadcast' : 'observation'}}"
aria-hidden="true"></i>
</button> </button>
</div> </div>
} }
<div class="col-auto ms-2"> <div class="col-auto ms-2">
<div class="card-actions btn-actions" [ngbTooltip]="t('more-alt')"> <div class="card-actions btn-actions" [ngbTooltip]="t('more-alt')">
<app-card-actionables [entity]="series" [inputActions]="seriesActions" [labelBy]="series.name" iconClass="fa-ellipsis-h" btnClass="btn"></app-card-actionables> <app-card-actionables [entity]="series" [inputActions]="seriesActions" [labelBy]="series.name"
iconClass="fa-ellipsis-h" btnClass="btn"></app-card-actionables>
</div> </div>
</div> </div>
<div class="col-auto ms-2 d-none d-md-block btn-actions"> <div class="col-auto ms-2 d-none d-md-block btn-actions download">
<app-download-button [download$]="download$" [entity]="series" entityType="series"></app-download-button> <app-download-button [download$]="download$" [entity]="series"
entityType="series"></app-download-button>
</div> </div>
</div> </div>
</div> </div>
<div class="mt-2 mb-3"> <div class="mt-2 mb-3">
<app-read-more [text]="seriesMetadata.summary || ''" [maxLength]="(utilityService.activeBreakpoint$ | async)! >= Breakpoint.Desktop ? 170 : 200"></app-read-more> <app-read-more [text]="seriesMetadata.summary || ''"
[maxLength]="(utilityService.activeBreakpoint$ | async)! >= Breakpoint.Desktop ? 170 : 200"></app-read-more>
</div> </div>
<div class="mt-2 upper-details"> <div class="mt-2 upper-details">
<div class="row g-0"> <div class="row g-0">
<div class="col-6 pe-5"> <div class="col-6 pe-5">
<span class="fw-bold">{{t('writers-title')}}</span> <span class="fw-bold">{{ t('writers-title') }}</span>
<div> <div>
<app-badge-expander [items]="seriesMetadata.writers" <app-badge-expander [items]="seriesMetadata.writers"
[itemsTillExpander]="3" [itemsTillExpander]="3"
[allowToggle]="false" [allowToggle]="false"
(toggle)="switchTabsToDetail()"> (toggle)="switchTabsToDetail()">
<ng-template #badgeExpanderItem let-item let-position="idx" let-last="last"> <ng-template #badgeExpanderItem let-item let-position="idx" let-last="last">
<a routerLink="/person/{{encodeURIComponent(item.name)}}/" class="dark-exempt btn-icon">{{item.name}}</a> <a routerLink="/person/{{encodeURIComponent(item.name)}}/"
class="dark-exempt btn-icon">{{ item.name }}</a>
</ng-template> </ng-template>
</app-badge-expander> </app-badge-expander>
</div> </div>
</div> </div>
<div class="col-6"> <div class="col-6">
<span class="fw-bold">{{t('publication-status-title')}}</span> <span class="fw-bold">{{ t('publication-status-title') }}</span>
<div> <div>
@if (seriesMetadata.publicationStatus | publicationStatus; as pubStatus) { @if (seriesMetadata.publicationStatus | publicationStatus; as pubStatus) {
<a class="dark-exempt btn-icon font-size" (click)="openFilter(FilterField.PublicationStatus, seriesMetadata!.publicationStatus)" <a class="dark-exempt btn-icon font-size"
(click)="openFilter(FilterField.PublicationStatus, seriesMetadata!.publicationStatus)"
href="javascript:void(0);" href="javascript:void(0);"
[ngbTooltip]="t('publication-status-tooltip') + (seriesMetadata.totalCount === 0 ? '' : ' (' + seriesMetadata.maxCount + ' / ' + seriesMetadata.totalCount + ')')"> [ngbTooltip]="t('publication-status-tooltip') + (seriesMetadata.totalCount === 0 ? '' : ' (' + seriesMetadata.maxCount + ' / ' + seriesMetadata.totalCount + ')')">
{{pubStatus}} {{ pubStatus }}
</a> </a>
} }
</div> </div>
@ -144,28 +160,30 @@
<div class="mt-3 mb-2 upper-details"> <div class="mt-3 mb-2 upper-details">
<div class="row g-0"> <div class="row g-0">
<div class="col-6 pe-5"> <div class="col-6 pe-5">
<span class="fw-bold">{{t('genres-title')}}</span> <span class="fw-bold">{{ t('genres-title') }}</span>
<div> <div>
<app-badge-expander [items]="seriesMetadata.genres" <app-badge-expander [items]="seriesMetadata.genres"
[itemsTillExpander]="3" [itemsTillExpander]="3"
[allowToggle]="false" [allowToggle]="false"
(toggle)="switchTabsToDetail()"> (toggle)="switchTabsToDetail()">
<ng-template #badgeExpanderItem let-item let-position="idx" let-last="last"> <ng-template #badgeExpanderItem let-item let-position="idx" let-last="last">
<a href="javascript:void(0)" class="dark-exempt btn-icon" (click)="openFilter(FilterField.Genres, item.id)">{{item.title}}</a> <a href="javascript:void(0)" class="dark-exempt btn-icon"
(click)="openFilter(FilterField.Genres, item.id)">{{ item.title }}</a>
</ng-template> </ng-template>
</app-badge-expander> </app-badge-expander>
</div> </div>
</div> </div>
<div class="col-6"> <div class="col-6">
<span class="fw-bold">{{t('tags-title')}}</span> <span class="fw-bold">{{ t('tags-title') }}</span>
<div> <div>
<app-badge-expander [items]="seriesMetadata.tags" <app-badge-expander [items]="seriesMetadata.tags"
[itemsTillExpander]="3" [itemsTillExpander]="3"
[allowToggle]="false" [allowToggle]="false"
(toggle)="switchTabsToDetail()"> (toggle)="switchTabsToDetail()">
<ng-template #badgeExpanderItem let-item let-position="idx" let-last="last"> <ng-template #badgeExpanderItem let-item let-position="idx" let-last="last">
<a href="javascript:void(0)" class="dark-exempt btn-icon" (click)="openFilter(FilterField.Tags, item.id)">{{item.title}}</a> <a href="javascript:void(0)" class="dark-exempt btn-icon"
(click)="openFilter(FilterField.Tags, item.id)">{{ item.title }}</a>
</ng-template> </ng-template>
</app-badge-expander> </app-badge-expander>
</div> </div>
@ -174,27 +192,33 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="carousel-tabs-container mb-2"> <div class="carousel-tabs-container mb-2">
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTabId" class="nav nav-tabs" [destroyOnHide]="false" (navChange)="onNavChange($event)"> <ul ngbNav #nav="ngbNav" [(activeId)]="activeTabId" class="nav nav-tabs" [destroyOnHide]="false"
(navChange)="onNavChange($event)">
@if (showStorylineTab) { @if (showStorylineTab) {
<li [ngbNavItem]="TabID.Storyline"> <li [ngbNavItem]="TabID.Storyline">
<a ngbNavLink>{{t(TabID.Storyline)}}</a> <a ngbNavLink>{{ t(TabID.Storyline) }}</a>
<ng-template ngbNavContent> <ng-template ngbNavContent>
@defer (when activeTabId === TabID.Storyline; prefetch on idle) { @defer (when activeTabId === TabID.Storyline; prefetch on idle) {
<virtual-scroller #scroll [items]="storylineItems" [bufferAmount]="1" [parentScroll]="scrollingBlock" [childHeight]="1"> <virtual-scroller #scroll [items]="storylineItems" [bufferAmount]="1" [parentScroll]="scrollingBlock"
[childHeight]="1">
<div class="card-container row g-0" #container> <div class="card-container row g-0" #container>
@for(item of scroll.viewPortItems; let idx = $index; track item) { @for (item of scroll.viewPortItems; let idx = $index; track item) {
@if (item.isChapter) { @if (item.isChapter) {
<ng-container [ngTemplateOutlet]="nonSpecialChapterCard" [ngTemplateOutletContext]="{$implicit: item.chapter, scroll: scroll, idx: idx, chaptersLength: storyChapters.length}"></ng-container> <ng-container [ngTemplateOutlet]="nonSpecialChapterCard"
[ngTemplateOutletContext]="{$implicit: item.chapter, scroll: scroll, idx: idx, chaptersLength: storyChapters.length}"></ng-container>
} @else { } @else {
<ng-container [ngTemplateOutlet]="nonChapterVolumeCard" [ngTemplateOutletContext]="{$implicit: item.volume, scroll: scroll, idx: idx, volumesLength: volumes.length}"></ng-container> <ng-container [ngTemplateOutlet]="nonChapterVolumeCard"
[ngTemplateOutletContext]="{$implicit: item.volume, scroll: scroll, idx: idx, volumesLength: volumes.length}"></ng-container>
} }
} }
<ng-container [ngTemplateOutlet]="estimatedNextCard" [ngTemplateOutletContext]="{tabId: TabID.Storyline}"></ng-container> <ng-container [ngTemplateOutlet]="estimatedNextCard"
[ngTemplateOutletContext]="{tabId: TabID.Storyline}"></ng-container>
</div> </div>
</virtual-scroller> </virtual-scroller>
} }
@ -206,17 +230,20 @@
@if (showVolumeTab) { @if (showVolumeTab) {
<li [ngbNavItem]="TabID.Volumes"> <li [ngbNavItem]="TabID.Volumes">
<a ngbNavLink> <a ngbNavLink>
{{UseBookLogic ? t('books-tab') : t('volumes-tab')}} {{ UseBookLogic ? t('books-tab') : t('volumes-tab') }}
<span class="badge rounded-pill text-bg-secondary">{{volumes.length}}</span> <span class="badge rounded-pill text-bg-secondary">{{ volumes.length }}</span>
</a> </a>
<ng-template ngbNavContent> <ng-template ngbNavContent>
@defer (when activeTabId === TabID.Volumes; prefetch on idle) { @defer (when activeTabId === TabID.Volumes; prefetch on idle) {
<virtual-scroller #scroll [items]="volumes" [parentScroll]="scrollingBlock" [childHeight]="1"> <virtual-scroller #scroll [items]="volumes" [parentScroll]="scrollingBlock" [childHeight]="1">
<div class="card-container row g-0" #container> <div class="card-container row g-0" #container>
@for (item of scroll.viewPortItems; let idx = $index; track item.id + '_' + item.pagesRead + + '_volumes') { @for (item of scroll.viewPortItems; let
<ng-container [ngTemplateOutlet]="nonChapterVolumeCard" [ngTemplateOutletContext]="{$implicit: item, scroll: scroll, idx: idx, totalLength: volumes.length}"></ng-container> idx = $index; track item.id + '_' + item.pagesRead + +'_volumes') {
<ng-container [ngTemplateOutlet]="nonChapterVolumeCard"
[ngTemplateOutletContext]="{$implicit: item, scroll: scroll, idx: idx, totalLength: volumes.length}"></ng-container>
} }
<ng-container [ngTemplateOutlet]="estimatedNextCard" [ngTemplateOutletContext]="{tabId: TabID.Volumes}"></ng-container> <ng-container [ngTemplateOutlet]="estimatedNextCard"
[ngTemplateOutletContext]="{tabId: TabID.Volumes}"></ng-container>
</div> </div>
</virtual-scroller> </virtual-scroller>
} }
@ -227,17 +254,20 @@
@if (showChapterTab) { @if (showChapterTab) {
<li [ngbNavItem]="TabID.Chapters"> <li [ngbNavItem]="TabID.Chapters">
<a ngbNavLink> <a ngbNavLink>
{{utilityService.formatChapterName(libraryType)}} {{ utilityService.formatChapterName(libraryType) }}
<span class="badge rounded-pill text-bg-secondary">{{chapters.length}}</span> <span class="badge rounded-pill text-bg-secondary">{{ chapters.length }}</span>
</a> </a>
<ng-template ngbNavContent> <ng-template ngbNavContent>
@defer (when activeTabId === TabID.Chapters; prefetch on idle) { @defer (when activeTabId === TabID.Chapters; prefetch on idle) {
<virtual-scroller #scroll [items]="chapters" [parentScroll]="scrollingBlock" [childHeight]="1"> <virtual-scroller #scroll [items]="chapters" [parentScroll]="scrollingBlock" [childHeight]="1">
<div class="card-container row g-0" #container> <div class="card-container row g-0" #container>
@for (item of scroll.viewPortItems; let idx = $index; track item.id + '_' + item.pagesRead + '_chapters') { @for (item of scroll.viewPortItems; let
<ng-container [ngTemplateOutlet]="nonSpecialChapterCard" [ngTemplateOutletContext]="{$implicit: item, scroll: scroll, idx: idx, totalLength: chapters.length}"></ng-container> idx = $index; track item.id + '_' + item.pagesRead + '_chapters') {
<ng-container [ngTemplateOutlet]="nonSpecialChapterCard"
[ngTemplateOutletContext]="{$implicit: item, scroll: scroll, idx: idx, totalLength: chapters.length}"></ng-container>
} }
<ng-container [ngTemplateOutlet]="estimatedNextCard" [ngTemplateOutletContext]="{tabId: TabID.Chapters}"></ng-container> <ng-container [ngTemplateOutlet]="estimatedNextCard"
[ngTemplateOutletContext]="{tabId: TabID.Chapters}"></ng-container>
</div> </div>
</virtual-scroller> </virtual-scroller>
} }
@ -248,15 +278,17 @@
@if (hasSpecials) { @if (hasSpecials) {
<li [ngbNavItem]="TabID.Specials"> <li [ngbNavItem]="TabID.Specials">
<a ngbNavLink> <a ngbNavLink>
{{t(TabID.Specials)}} {{ t(TabID.Specials) }}
<span class="badge rounded-pill text-bg-secondary">{{specials.length}}</span> <span class="badge rounded-pill text-bg-secondary">{{ specials.length }}</span>
</a> </a>
<ng-template ngbNavContent> <ng-template ngbNavContent>
@defer (when activeTabId === TabID.Specials; prefetch on idle) { @defer (when activeTabId === TabID.Specials; prefetch on idle) {
<virtual-scroller #scroll [items]="specials" [parentScroll]="scrollingBlock" [childHeight]="1"> <virtual-scroller #scroll [items]="specials" [parentScroll]="scrollingBlock" [childHeight]="1">
<div class="card-container row g-0" #container> <div class="card-container row g-0" #container>
@for(item of scroll.viewPortItems; let idx = $index; track item.id + '_' + item.pagesRead + '_specials') { @for (item of scroll.viewPortItems; let
<ng-container [ngTemplateOutlet]="specialChapterCard" [ngTemplateOutletContext]="{$implicit: item, scroll: scroll, idx: idx, chaptersLength: chapters.length}"></ng-container> idx = $index; track item.id + '_' + item.pagesRead + '_specials') {
<ng-container [ngTemplateOutlet]="specialChapterCard"
[ngTemplateOutletContext]="{$implicit: item, scroll: scroll, idx: idx, chaptersLength: chapters.length}"></ng-container>
} }
</div> </div>
</virtual-scroller> </virtual-scroller>
@ -268,8 +300,8 @@
@if (hasRelations || readingLists.length > 0 || collections.length > 0 || bookmarks.length > 0) { @if (hasRelations || readingLists.length > 0 || collections.length > 0 || bookmarks.length > 0) {
<li [ngbNavItem]="TabID.Related"> <li [ngbNavItem]="TabID.Related">
<a ngbNavLink> <a ngbNavLink>
{{t(TabID.Related)}} {{ t(TabID.Related) }}
<span class="badge rounded-pill text-bg-secondary">{{relations.length + readingLists.length + collections.length + (bookmarks.length > 0 ? 1 : 0)}}</span> <span class="badge rounded-pill text-bg-secondary">{{ relations.length + readingLists.length + collections.length + (bookmarks.length > 0 ? 1 : 0) }}</span>
</a> </a>
<ng-template ngbNavContent> <ng-template ngbNavContent>
@defer (when activeTabId === TabID.Related; prefetch on idle) { @defer (when activeTabId === TabID.Related; prefetch on idle) {
@ -286,18 +318,20 @@
@if (hasRecommendations) { @if (hasRecommendations) {
<li [ngbNavItem]="TabID.Recommendations"> <li [ngbNavItem]="TabID.Recommendations">
<a ngbNavLink> <a ngbNavLink>
{{t(TabID.Recommendations)}} {{ t(TabID.Recommendations) }}
<span class="badge rounded-pill text-bg-secondary">{{combinedRecs.length}}</span> <span class="badge rounded-pill text-bg-secondary">{{ combinedRecs.length }}</span>
</a> </a>
<ng-template ngbNavContent> <ng-template ngbNavContent>
@defer (when activeTabId === TabID.Recommendations; prefetch on idle) { @defer (when activeTabId === TabID.Recommendations; prefetch on idle) {
<virtual-scroller #scroll [items]="combinedRecs" [parentScroll]="scrollingBlock" [childHeight]="1"> <virtual-scroller #scroll [items]="combinedRecs" [parentScroll]="scrollingBlock" [childHeight]="1">
<div class="card-container row g-0" #container> <div class="card-container row g-0" #container>
@for(item of scroll.viewPortItems; let idx = $index; track idx) { @for (item of scroll.viewPortItems; let idx = $index; track idx) {
@if (!item.hasOwnProperty('coverUrl')) { @if (!item.hasOwnProperty('coverUrl')) {
<app-series-card class="col-auto mt-2 mb-2" [series]="item" [previewOnClick]="true" [libraryId]="item.libraryId"></app-series-card> <app-series-card class="col-auto mt-2 mb-2" [series]="item" [previewOnClick]="true"
[libraryId]="item.libraryId"></app-series-card>
} @else { } @else {
<app-external-series-card class="col-auto mt-2 mb-2" [previewOnClick]="true" [data]="item"></app-external-series-card> <app-external-series-card class="col-auto mt-2 mb-2" [previewOnClick]="true"
[data]="item"></app-external-series-card>
} }
} }
</div> </div>
@ -309,19 +343,19 @@
<li [ngbNavItem]="TabID.Reviews"> <li [ngbNavItem]="TabID.Reviews">
<a ngbNavLink> <a ngbNavLink>
{{t(TabID.Reviews)}} {{ t(TabID.Reviews) }}
<span class="badge rounded-pill text-bg-secondary">{{reviews.length + plusReviews.length}}</span> <span class="badge rounded-pill text-bg-secondary">{{ reviews.length + plusReviews.length }}</span>
</a> </a>
<ng-template ngbNavContent> <ng-template ngbNavContent>
@defer (when activeTabId === TabID.Reviews; prefetch on idle) { @defer (when activeTabId === TabID.Reviews; prefetch on idle) {
<app-reviews [userReviews]="reviews" [plusReviews]="plusReviews" [series]="series" /> <app-reviews [userReviews]="reviews" [plusReviews]="plusReviews" [series]="series"/>
} }
</ng-template> </ng-template>
</li> </li>
@if (seriesMetadata && showDetailsTab) { @if (seriesMetadata && showDetailsTab) {
<li [ngbNavItem]="TabID.Details" id="details-tab"> <li [ngbNavItem]="TabID.Details" id="details-tab">
<a ngbNavLink>{{t(TabID.Details)}}</a> <a ngbNavLink>{{ t(TabID.Details) }}</a>
<ng-template ngbNavContent> <ng-template ngbNavContent>
@defer (when activeTabId === TabID.Details; prefetch on idle) { @defer (when activeTabId === TabID.Details; prefetch on idle) {
<app-details-tab [metadata]="seriesMetadata" <app-details-tab [metadata]="seriesMetadata"
@ -354,10 +388,12 @@
} }
} }
@case (TabID.Chapters) { @case (TabID.Chapters) {
<app-next-expected-card class="col-auto mt-2 mb-2" [entity]="nextExpectedChapter" [imageUrl]="imageService.getSeriesCoverImage(series.id)"></app-next-expected-card> <app-next-expected-card class="col-auto mt-2 mb-2" [entity]="nextExpectedChapter"
[imageUrl]="imageService.getSeriesCoverImage(series.id)"></app-next-expected-card>
} }
@case (TabID.Storyline) { @case (TabID.Storyline) {
<app-next-expected-card class="col-auto mt-2 mb-2" [entity]="nextExpectedChapter" [imageUrl]="imageService.getSeriesCoverImage(series.id)"></app-next-expected-card> <app-next-expected-card class="col-auto mt-2 mb-2" [entity]="nextExpectedChapter"
[imageUrl]="imageService.getSeriesCoverImage(series.id)"></app-next-expected-card>
} }
} }
} }
@ -373,15 +409,18 @@
[actions]="chapterActions" [actions]="chapterActions"
[libraryType]="libraryType" [libraryType]="libraryType"
(selection)="bulkSelectionService.handleCardSelection('chapter', scroll.viewPortInfo.startIndexWithBuffer + idx, totalLength, $event)" (selection)="bulkSelectionService.handleCardSelection('chapter', scroll.viewPortInfo.startIndexWithBuffer + idx, totalLength, $event)"
[selected]="bulkSelectionService.isCardSelected('chapter', scroll.viewPortInfo.startIndexWithBuffer + idx)" [allowSelection]="true"> [selected]="bulkSelectionService.isCardSelected('chapter', scroll.viewPortInfo.startIndexWithBuffer + idx)"
[allowSelection]="true">
</app-chapter-card> </app-chapter-card>
</ng-template> </ng-template>
<ng-template #nonChapterVolumeCard let-item let-scroll="scroll" let-idx="idx" let-totalLength="totalLength"> <ng-template #nonChapterVolumeCard let-item let-scroll="scroll" let-idx="idx" let-totalLength="totalLength">
<app-volume-card class="col-auto mt-2 mb-2" [volume]="item" [seriesId]="seriesId" [libraryId]="libraryId" [libraryType]="libraryType" <app-volume-card class="col-auto mt-2 mb-2" [volume]="item" [seriesId]="seriesId" [libraryId]="libraryId"
[libraryType]="libraryType"
[actions]="volumeActions" [actions]="volumeActions"
(selection)="bulkSelectionService.handleCardSelection('volume', scroll.viewPortInfo.startIndexWithBuffer + idx, totalLength, $event)" (selection)="bulkSelectionService.handleCardSelection('volume', scroll.viewPortInfo.startIndexWithBuffer + idx, totalLength, $event)"
[selected]="bulkSelectionService.isCardSelected('volume', scroll.viewPortInfo.startIndexWithBuffer + idx)" [allowSelection]="true"> [selected]="bulkSelectionService.isCardSelected('volume', scroll.viewPortInfo.startIndexWithBuffer + idx)"
[allowSelection]="true">
</app-volume-card> </app-volume-card>
</ng-template> </ng-template>
@ -391,6 +430,7 @@
[actions]="chapterActions" [actions]="chapterActions"
[libraryType]="libraryType" [libraryType]="libraryType"
(selection)="bulkSelectionService.handleCardSelection('special', scroll.viewPortInfo.startIndexWithBuffer + idx, totalLength, $event)" (selection)="bulkSelectionService.handleCardSelection('special', scroll.viewPortInfo.startIndexWithBuffer + idx, totalLength, $event)"
[selected]="bulkSelectionService.isCardSelected('special', scroll.viewPortInfo.startIndexWithBuffer + idx)" [allowSelection]="true"> [selected]="bulkSelectionService.isCardSelected('special', scroll.viewPortInfo.startIndexWithBuffer + idx)"
[allowSelection]="true">
</app-chapter-card> </app-chapter-card>
</ng-template> </ng-template>

View file

@ -16,6 +16,7 @@ $accordion-icon-active-color: #cecece;
$grid-breakpoints-xs: 0; $grid-breakpoints-xs: 0;
$grid-breakpoints-smm: 350px;
$grid-breakpoints-sm: 576px; $grid-breakpoints-sm: 576px;
$grid-breakpoints-md: 768px; $grid-breakpoints-md: 768px;
$grid-breakpoints-lg: 992px; $grid-breakpoints-lg: 992px;

View file

@ -110,7 +110,7 @@
} }
} }
.btn-actions { .btn-actions:not(.download) {
color: var(--btn-fa-icon-color); color: var(--btn-fa-icon-color);
border-radius: var(--btn-actions-border-radius); border-radius: var(--btn-actions-border-radius);

View file

@ -87,7 +87,7 @@
--theme-color: #000000; --theme-color: #000000;
--color-scheme: dark; --color-scheme: dark;
--tile-color: var(--primary-color); --tile-color: var(--primary-color);
--nav-offset: 60px; --nav-offset: 48px;
--nav-mobile-offset: 55px; --nav-mobile-offset: 55px;
/* Should we render the series cover as background on mobile */ /* Should we render the series cover as background on mobile */