Lots of Bugfixes (#2960)

Co-authored-by: Samuel Martins <s@smartins.ch>
Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
This commit is contained in:
Joe Milazzo 2024-05-22 06:58:23 -05:00 committed by GitHub
parent 97ffdd0975
commit b50fa0fd1e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
45 changed files with 563 additions and 282 deletions

View file

@ -1,4 +1,4 @@
$scrollbarHeight: 34px;
$scrollbarHeight: 35px;
img {
user-select: none;
@ -9,29 +9,31 @@ img {
align-items: center;
&.full-width {
height: calc(var(--vh)*100);
height: 100dvh;
display: grid;
}
&.full-height {
height: calc(100vh); // We need to - $scrollbarHeight when there is a horizontal scroll on macos
height: calc(100dvh); // We need to - $scrollbarHeight when there is a horizontal scroll on macos
display: flex;
align-content: center;
overflow-y: hidden;
}
&.original {
height: 100vh;
height: calc(100dvh);
display: grid;
}
.full-height {
width: auto;
margin: auto;
max-height: calc(var(--vh)*100);
overflow: hidden; // This technically will crop and make it just fit
max-height: calc(100dvh);
height: calc(100dvh);
vertical-align: top;
object-fit: cover;
&.wide {
height: 100vh;
height: calc(100dvh);
}
}
@ -46,12 +48,13 @@ img {
width: 100%;
margin: 0 auto;
vertical-align: top;
max-width: fit-content;
object-fit: contain;
width: 100%;
}
.fit-to-screen.full-width {
width: 100%;
max-height: calc(var(--vh)*100);
max-height: calc(100dvh);
}
}

View file

@ -10,7 +10,6 @@ import { Volume } from '../_models/volume';
import { AccountService } from './account.service';
import { DeviceService } from './device.service';
import {SideNavStream} from "../_models/sidenav/sidenav-stream";
import {User} from "../_models/user";
export enum Action {
Submenu = -1,

View file

@ -103,7 +103,11 @@ export enum EVENTS {
/**
* A Theme was updated and UI should refresh to get the latest version
*/
SiteThemeUpdated= 'SiteThemeUpdated'
SiteThemeUpdated = 'SiteThemeUpdated',
/**
* A Progress event when a smart collection is synchronizing
*/
SmartCollectionSync = 'SmartCollectionSync'
}
export interface Message<T> {
@ -199,6 +203,13 @@ export class MessageHubService {
});
});
this.hubConnection.on(EVENTS.SmartCollectionSync, resp => {
this.messagesSource.next({
event: EVENTS.NotificationProgress,
payload: resp.body
});
});
this.hubConnection.on(EVENTS.SiteThemeUpdated, resp => {
this.messagesSource.next({
event: EVENTS.SiteThemeUpdated,

View file

@ -24,11 +24,11 @@
<div class="card-footer bg-transparent text-muted">
<div>
@if (isMyReview) {
<i class="d-md-none fa-solid fa-star me-1" aria-hidden="true" [title]="t('your-review')"></i>
<img class="me-1" [ngSrc]="ScrobbleProvider.Kavita | providerImage" width="20" height="20" alt="">
<i class="d-md-none fa-solid fa-star me-2" aria-hidden="true" [title]="t('your-review')"></i>
<img class="me-2" [ngSrc]="ScrobbleProvider.Kavita | providerImage" width="20" height="20" alt="">
{{review.username}}
} @else {
<img class="me-1" [ngSrc]="review.provider | providerImage" width="20" height="20" alt="">
<img class="me-2" [ngSrc]="review.provider | providerImage" width="20" height="20" alt="">
}
{{(isMyReview ? '' : review.username | defaultValue:'')}}

View file

@ -42,4 +42,10 @@
max-width: 319px;
justify-content: space-between;
margin: 0 auto;
padding: .5rem 0;
& > * {
margin: 0 5px;
display: inline-flex;
}
}

View file

@ -1,6 +1,7 @@
<ng-container *transloco="let t; read:'user-scrobble-history'">
<h5>{{t('title')}}</h5>
<p>{{t('description')}}</p>
<p class="fw-bold">{{t('not-read-warning')}}</p>
<div class="row g-0 mb-2">
<div class="col-md-10">
<form [formGroup]="formGroup">
@ -67,7 +68,11 @@
@switch (item.scrobbleEventType) {
@case (ScrobbleEventType.ChapterRead) {
@if(item.volumeNumber === LooseLeafOrDefaultNumber) {
{{t('chapter-num', {num: item.chapterNumber})}}
@if (item.chapterNumber === LooseLeafOrDefaultNumber) {
{{t('special')}}
} @else {
{{t('chapter-num', {num: item.chapterNumber})}}
}
}
@else if (item.chapterNumber === LooseLeafOrDefaultNumber) {
{{t('volume-num', {num: item.volumeNumber})}}

View file

@ -3,8 +3,12 @@
<form [formGroup]="settingsForm" *ngIf="serverSettings !== undefined">
<h4 id="email-header">{{t('title')}}</h4>
<p>You must fill out both Host Name and SMTP settings to use email-based functionality within Kavita.</p>
<p>{{t('setting-description')}}</p>
@if (settingsForm.dirty) {
<ngb-alert [type]="'warning'">
{{t('test-warning')}}
</ngb-alert>
}
<div class="mb-3 pe-2 ps-2 ">
<label for="settings-hostname" class="form-label">{{t('host-name-label')}}</label><i class="fa fa-info-circle ms-1" placement="right" [ngbTooltip]="hostNameTooltip" role="button" tabindex="0"></i>
<ng-template #hostNameTooltip>{{t('host-name-tooltip')}}</ng-template>

View file

@ -5,6 +5,7 @@ import {take} from 'rxjs';
import {SettingsService} from '../settings.service';
import {ServerSettings} from '../_models/server-settings';
import {
NgbAlert,
NgbTooltip
} from '@ng-bootstrap/ng-bootstrap';
import {NgIf, NgTemplateOutlet, TitleCasePipe} from '@angular/common';
@ -19,7 +20,7 @@ import {ManageAlertsComponent} from "../manage-alerts/manage-alerts.component";
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [NgIf, ReactiveFormsModule, NgbTooltip, NgTemplateOutlet, TranslocoModule, SafeHtmlPipe,
ManageAlertsComponent, TitleCasePipe]
ManageAlertsComponent, TitleCasePipe, NgbAlert]
})
export class ManageEmailSettingsComponent implements OnInit {

View file

@ -14,7 +14,7 @@
<ng-template #cardItem let-item let-position="idx">
<!-- TODO: figure a way to get a hover effect -->
<div class="card-item-container card clickable" (click)="loadSmartFilter(item)">
<div class="overlay">
<div class="overlay filter">
<div class="card-overlay"></div>
<div class="overlay-information overlay-information--centered">
<div class="position-relative">

View file

@ -1,7 +1,7 @@
<ng-container *transloco="let t; read: 'series-info-cards'">
<div class="row g-0 mt-3">
<ng-container *ngIf="seriesMetadata.releaseYear > 0">
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
<div class="col-xl-auto col-lg-auto col-md-4 col-sm-4 col-4 mb-2">
<app-icon-and-title [label]="t('release-date-title')" [clickable]="false" fontClasses="fa-regular fa-calendar" [title]="t('release-year-tooltip')">
{{seriesMetadata.releaseYear}}
</app-icon-and-title>
@ -11,7 +11,7 @@
<ng-container *ngIf="seriesMetadata">
<ng-container *ngIf="seriesMetadata.ageRating">
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
<div class="col-xl-auto col-lg-auto col-md-4 col-sm-4 col-4 mb-2">
<app-icon-and-title [label]="t('age-rating-title')" [clickable]="true" fontClasses="fas fa-eye" (click)="handleGoTo(FilterField.AgeRating, seriesMetadata.ageRating)" [title]="t('age-rating-title')">
{{this.seriesMetadata.ageRating | ageRating}}
</app-icon-and-title>
@ -20,7 +20,7 @@
</ng-container>
<ng-container *ngIf="seriesMetadata.language !== null && seriesMetadata.language !== ''">
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
<div class="col-xl-auto col-lg-auto col-md-4 col-sm-4 col-4 mb-2">
<app-icon-and-title [label]="t('language-title')" [clickable]="true" fontClasses="fas fa-language" (click)="handleGoTo(FilterField.Languages, seriesMetadata.language)" [title]="t('language-title')">
{{seriesMetadata.language | defaultValue:'en' | languageName | async}}
</app-icon-and-title>
@ -30,7 +30,7 @@
</ng-container>
<ng-container>
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
<div class="col-xl-auto col-lg-auto col-md-4 col-sm-4 col-4 mb-2">
<ng-container *ngIf="seriesMetadata.publicationStatus | publicationStatus as pubStatus">
<app-icon-and-title [label]="t('publication-status-title')" [clickable]="true" fontClasses="fa-solid fa-hourglass-{{pubStatus === t('ongoing') ? 'empty' : 'end'}}"
(click)="handleGoTo(FilterField.PublicationStatus, seriesMetadata.publicationStatus)"
@ -43,7 +43,7 @@
</ng-container>
<ng-container *ngIf="accountService.hasValidLicense$ | async">
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
<div class="col-xl-auto col-lg-auto col-md-4 col-sm-4 col-4 mb-2">
<app-icon-and-title [label]="t('scrobbling-title')" [clickable]="libraryAllowsScrobbling"
fontClasses="fa-solid fa-tower-{{(isScrobbling && libraryAllowsScrobbling) ? 'broadcast' : 'observation'}}"
(click)="toggleScrobbling($event)"
@ -62,7 +62,7 @@
<ng-container *ngIf="series">
<ng-container>
<div class="d-none d-md-block col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
<div class="d-none d-md-block col-xl-auto col-lg-auto col-md-4 col-sm-4 col-4 mb-2">
<app-icon-and-title [label]="t('format-title')" [clickable]="true"
[fontClasses]="series.format | mangaFormatIcon"
(click)="handleGoTo(FilterField.Formats, series.format)" [title]="t('format-title')">
@ -73,7 +73,7 @@
</ng-container>
<ng-container *ngIf="series.latestReadDate && series.latestReadDate !== '' && (series.latestReadDate | date: 'shortDate') !== '1/1/01'">
<div class="d-none d-md-block col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
<div class="d-none d-md-block col-xl-auto col-lg-auto col-md-4 col-sm-4 col-4 mb-2">
<app-icon-and-title [label]="t('last-read-title')" [clickable]="false" fontClasses="fa-regular fa-clock" [title]="t('last-read-title')">
{{series.latestReadDate | timeAgo}}
</app-icon-and-title>
@ -83,7 +83,7 @@
<ng-container *ngIf="series.format === MangaFormat.EPUB; else showPages">
<ng-container *ngIf="series.wordCount > 0">
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
<div class="col-xl-auto col-lg-auto col-md-4 col-sm-4 col-4 mb-2">
<app-icon-and-title [label]="t('length-title')" [clickable]="false" fontClasses="fa-solid fa-book-open">
{{t('words-count', {num: series.wordCount | compactNumber})}}
</app-icon-and-title>
@ -93,7 +93,7 @@
</ng-container>
<ng-template #showPages>
<div class="d-none d-md-block col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
<div class="d-none d-md-block col-xl-auto col-lg-auto col-md-4 col-sm-4 col-4 mb-2">
<app-icon-and-title [label]="t('length-title')" [clickable]="false" fontClasses="fa-regular fa-file-lines">
{{t('pages-count', {num: series.pages | compactNumber})}}
</app-icon-and-title>
@ -102,7 +102,7 @@
</ng-template>
<ng-container *ngIf="series.format === MangaFormat.EPUB && series.wordCount > 0 || series.format !== MangaFormat.EPUB">
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
<div class="col-xl-auto col-lg-auto col-md-4 col-sm-4 col-4 mb-2">
<app-icon-and-title [label]="t('read-time-title')" [clickable]="false" fontClasses="fa-regular fa-clock">
<ng-container *ngIf="readingTime.maxHours === 0 || readingTime.minHours === 0; else normalReadTime">{{t('less-than-hour')}}</ng-container>
<ng-template #normalReadTime>
@ -114,7 +114,7 @@
<ng-container *ngIf="hasReadingProgress && showReadingTimeLeft && readingTimeLeft && readingTimeLeft.avgHours !== 0">
<div class="vr d-none d-lg-block m-2"></div>
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
<div class="col-xl-auto col-lg-auto col-md-4 col-sm-4 col-4 mb-2">
<app-icon-and-title label="Time Left" [clickable]="false" fontClasses="fa-solid fa-clock">
~{{readingTimeLeft.avgHours}} {{readingTimeLeft.avgHours > 1 ? t('hours') : t('hour')}}
</app-icon-and-title>

View file

@ -6,11 +6,13 @@ import {NgbActiveModal} from "@ng-bootstrap/ng-bootstrap";
import {CollectionTagService} from "../../../_services/collection-tag.service";
import {MalStack} from "../../../_models/collection/mal-stack";
import {UserCollection} from "../../../_models/collection-tag";
import {ScrobbleProvider} from "../../../_services/scrobbling.service";
import {ScrobbleProvider, ScrobblingService} from "../../../_services/scrobbling.service";
import {forkJoin} from "rxjs";
import {ToastrService} from "ngx-toastr";
import {DecimalPipe} from "@angular/common";
import {LoadingComponent} from "../../../shared/loading/loading.component";
import {AccountService} from "../../../_services/account.service";
import {ConfirmService} from "../../../shared/confirm.service";
@Component({
selector: 'app-import-mal-collection-modal',
@ -32,13 +34,25 @@ export class ImportMalCollectionModalComponent {
private readonly collectionService = inject(CollectionTagService);
private readonly cdRef = inject(ChangeDetectorRef);
private readonly toastr = inject(ToastrService);
private readonly scrobblingService = inject(ScrobblingService);
private readonly confirmService = inject(ConfirmService);
stacks: Array<MalStack> = [];
isLoading = true;
collectionMap: {[key: string]: UserCollection | MalStack} = {};
constructor() {
this.scrobblingService.getMalToken().subscribe(async token => {
if (token.accessToken === '') {
await this.confirmService.alert(translate('toasts.mal-token-required'));
this.ngbModal.dismiss();
return;
}
this.setup();
});
}
setup() {
forkJoin({
allCollections: this.collectionService.allCollections(true),
malStacks: this.collectionService.getMalStacks()

View file

@ -3,18 +3,25 @@
.image-container {
#image-1 {
&.double {
margin: 0 0 0 auto;
}
}
}
.image-container.full-height {
display: inline-block !important;
.image-container {
&.full-height {
display: flex;
align-content: center;
justify-content: center;
}
.full-height {
margin: unset;
object-fit: contain;
}
}
.full-width {
width: 100%;
margin: 0 auto;
vertical-align: top;
max-width: fit-content;
@ -41,7 +48,7 @@
}
.fit-to-height-double-offset {
height: 100vh;
height: calc(100dvh);
object-fit: scale-down;
top: 50%;
left: 50%;

View file

@ -2,7 +2,7 @@
// Overrides for reverse
.image-container {
height: calc(100vh); // override as on single, we -34px for the potential scrollbar
height: calc(100dvh); // override as on single, we -34px for the potential scrollbar
&.reverse {
overflow: unset;
@ -29,8 +29,16 @@
}
}
.image-container.full-height {
display: inline-block;
.image-container {
display: flex;
align-content: center;
justify-content: center;
.full-height {
margin: unset;
object-fit: contain;
}
}
.full-width {
@ -62,7 +70,7 @@
}
.fit-to-height-double-offset {
height: 100vh;
height: calc(100dvh);
object-fit: scale-down;
top: 50%;
left: 50%;

View file

@ -41,7 +41,7 @@
<app-loading [loading]="isLoading || (!(currentImage$ | async)?.complete && this.readerMode !== ReaderMode.Webtoon)" [absolute]="true"></app-loading>
<div class="reading-area"
ngSwipe (swipeEnd)="onSwipeEnd($event)" (swipeMove)="onSwipeMove($event)"
[ngStyle]="{'background-color': backgroundColor, 'height': readerMode === ReaderMode.Webtoon ? 'inherit' : 'calc(var(--vh)*100)'}" #readingArea>
[ngStyle]="{'background-color': backgroundColor, 'height': readerMode === ReaderMode.Webtoon ? 'inherit' : '100dvh'}" #readingArea>
<ng-container *ngIf="readerMode !== ReaderMode.Webtoon; else webtoon">
<div (dblclick)="bookmarkPage($event)">
@ -56,14 +56,14 @@
<!-- Pagination controls and screen hints-->
<div class="pagination-area">
<div class="{{readerMode === ReaderMode.LeftRight ? 'left' : 'top'}} {{clickOverlayClass('left')}}" (click)="handlePageChange($event, KeyDirection.Left)"
[ngStyle]="{'height': (readerMode === ReaderMode.LeftRight ? ImageHeight: '25%'), 'max-height': MaxHeight}">
[ngStyle]="{'height': (readerMode === ReaderMode.LeftRight ? MaxHeight: '25%'), 'max-height': MaxHeight}">
<div *ngIf="showClickOverlay">
<i class="fa fa-angle-{{readingDirection === ReadingDirection.RightToLeft ? 'double-' : ''}}{{readerMode === ReaderMode.LeftRight ? 'left' : 'up'}}"
[title]="t('prev-page-tooltip')" aria-hidden="true"></i>
</div>
</div>
<div class="{{readerMode === ReaderMode.LeftRight ? 'right' : 'bottom'}} {{clickOverlayClass('right')}}" (click)="handlePageChange($event, KeyDirection.Right)"
[ngStyle]="{'height': (readerMode === ReaderMode.LeftRight ? ImageHeight: '25%'),
[ngStyle]="{'height': (readerMode === ReaderMode.LeftRight ? MaxHeight: '25%'),
'left': 'inherit',
'right': RightPaginationOffset + 'px',
'max-height': MaxHeight}">

View file

@ -16,7 +16,6 @@ $pointer-offset: 5px;
.reading-area {
position: relative;
overflow: auto;
text-align: center;
//height: calc(var(--vh)*100); // this needs to be applied on the DOM because it breaks infinite scroller

View file

@ -432,17 +432,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
// This is for the pagination area
get MaxHeight() {
if (this.FittingOption === FITTING_OPTION.HEIGHT) {
return 'calc(var(--vh) * 100)';
}
const needsScrolling = this.readingArea?.nativeElement?.scrollHeight > this.readingArea?.nativeElement?.clientHeight;
if (this.readingArea?.nativeElement?.clientHeight <= this.mangaReaderService.getPageDimensions(this.pageNum)?.height!) {
if (needsScrolling) {
return Math.min(this.readingArea?.nativeElement?.scrollHeight, this.mangaReaderService.getPageDimensions(this.pageNum)?.height!) + 'px';
}
}
return this.readingArea?.nativeElement?.clientHeight + 'px';
return '100dvh';
}
get RightPaginationOffset() {

View file

@ -150,14 +150,14 @@ export class SingleRendererComponent implements OnInit, ImageRenderer {
if (mode !== FITTING_OPTION.HEIGHT) return '';
const readingArea = this.document.querySelector('.reading-area');
if (!readingArea) return 'calc(100vh)';
if (!readingArea) return 'calc(100dvh)';
// If you ever see fit to height and a bit of scrollbar, it's due to currentImage not being ready on first load
if (this.currentImage?.width - readingArea.scrollWidth > 0) {
// we also need to check if this is FF or Chrome. FF doesn't require the -34px as it doesn't render a scrollbar
return 'calc(100vh - 34px)';
return 'calc(100dvh)';
}
return 'calc(100vh)';
return 'calc(100dvh)';
}),
filter(_ => this.isValid())
);

View file

@ -118,7 +118,7 @@ export class EventsWidgetComponent implements OnInit, OnDestroy {
this.cdRef.markForCheck();
break;
case 'started':
// Sometimes we can receive 2 started on long running scans, so better to just treat as a merge then.
// Sometimes we can receive 2 started on long-running scans, so better to just treat as a merge then.
data = this.mergeOrUpdate(this.progressEventsSource.getValue(), message);
this.progressEventsSource.next(data);
break;

View file

@ -3,137 +3,145 @@
<h2 title>
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [attr.aria-labelledby]="readingList?.title" *ngIf="actions.length > 0"></app-card-actionables>
{{readingList?.title}}
<span *ngIf="readingList?.promoted" class="ms-1">(<i class="fa fa-angle-double-up" aria-hidden="true"></i>)</span>
@if (readingList?.promoted) {
<span class="ms-1">(<i class="fa fa-angle-double-up" aria-hidden="true"></i>)</span>
}
</h2>
<h6 subtitle class="subtitle-with-actionables">{{t('item-count', {num: items.length | number})}}</h6>
<ng-template #extrasDrawer let-offcanvas>
<div style="margin-top: 56px" *ngIf="readingList">
<div class="offcanvas-header">
<h4 class="offcanvas-title" id="offcanvas-basic-title">{{t('page-settings-title')}}</h4>
<button type="button" class="btn-close" aria-label="Close" (click)="offcanvas.dismiss()"></button>
</div>
<div class="offcanvas-body">
<div class="row g-0">
<div class="col-md-12 col-sm-12 pe-2 mb-3">
<button class="btn btn-danger" (click)="removeRead()" [disabled]="readingList.promoted && !this.isAdmin">
@if (readingList) {
<div style="margin-top: 56px">
<div class="offcanvas-header">
<h4 class="offcanvas-title" id="offcanvas-basic-title">{{t('page-settings-title')}}</h4>
<button type="button" class="btn-close" aria-label="Close" (click)="offcanvas.dismiss()"></button>
</div>
<div class="offcanvas-body">
<div class="row g-0">
<div class="col-md-12 col-sm-12 pe-2 mb-3">
<button class="btn btn-danger" (click)="removeRead()" [disabled]="readingList.promoted && !this.isAdmin">
<span>
<i class="fa fa-check"></i>
</span>
<span class="read-btn--text">&nbsp;{{t('remove-read')}}</span>
</button>
<span class="read-btn--text">&nbsp;{{t('remove-read')}}</span>
</button>
<div class="col-auto ms-2 mt-2" *ngIf="!(readingList?.promoted && !this.isAdmin)">
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="accessibility-mode" [value]="accessibilityMode" (change)="updateAccessibilityMode()">
<label class="form-check-label" for="accessibility-mode">{{t('order-numbers-label')}}</label>
</div>
@if (!(readingList.promoted && !this.isAdmin)) {
<div class="col-auto ms-2 mt-2">
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="accessibility-mode" [disabled]="this.utilityService.getActiveBreakpoint() < Breakpoint.Tablet" [value]="accessibilityMode" (change)="updateAccessibilityMode()">
<label class="form-check-label" for="accessibility-mode">{{t('order-numbers-label')}}</label>
</div>
</div>
}
</div>
</div>
</div>
</div>
</div>
}
</ng-template>
</app-side-nav-companion-bar>
<div class="container-fluid mt-2" *ngIf="readingList" >
<div class="row mb-2">
<div class="col-md-2 col-xs-4 col-sm-6 d-none d-sm-block">
<app-image [styles]="{'max-height': '400px', 'max-width': '300px'}" [imageUrl]="imageService.getReadingListCoverImage(readingList.id)"></app-image>
</div>
<div class="col-md-10 col-xs-8 col-sm-6 mt-2">
<div class="row g-0 mb-3">
<div class="col-auto me-2">
<!-- Action row-->
<div class="btn-group me-3">
<button type="button" class="btn btn-primary" (click)="continue()">
@if (readingList) {
<div class="container-fluid mt-2">
<div class="row mb-2">
<div class="col-md-2 col-xs-4 col-sm-6 d-none d-sm-block">
<app-image [styles]="{'max-height': '400px', 'max-width': '300px'}" [imageUrl]="imageService.getReadingListCoverImage(readingList.id)"></app-image>
</div>
<div class="col-md-10 col-xs-8 col-sm-6 mt-2">
<div class="row g-0 mb-3">
<div class="col-auto me-2">
<!-- Action row-->
<div class="btn-group me-3">
<button type="button" class="btn btn-primary" (click)="continue()">
<span>
<i class="fa fa-book-open me-1" aria-hidden="true"></i>
<span class="read-btn--text">{{t('continue')}}</span>
</span>
</button>
<div class="btn-group" ngbDropdown role="group" [attr.aria-label]="t('read-options-alt')">
<button type="button" class="btn btn-primary dropdown-toggle-split" ngbDropdownToggle></button>
<div class="dropdown-menu" ngbDropdownMenu>
<button ngbDropdownItem (click)="read()">
</button>
<div class="btn-group" ngbDropdown role="group" [attr.aria-label]="t('read-options-alt')">
<button type="button" class="btn btn-primary dropdown-toggle-split" ngbDropdownToggle></button>
<div class="dropdown-menu" ngbDropdownMenu>
<button ngbDropdownItem (click)="read()">
<span>
<i class="fa fa-book" aria-hidden="true"></i>
<span class="read-btn--text">&nbsp;{{t('read')}}</span>
</span>
</button>
<button ngbDropdownItem (click)="continue(true)">
</button>
<button ngbDropdownItem (click)="continue(true)">
<span>
<i class="fa fa-book-open me-1" aria-hidden="true"></i>
<span class="read-btn--text">{{t('continue')}}</span>
(<i class="fa fa-glasses ms-1" aria-hidden="true"></i>)
<span class="visually-hidden">{{t('incognito-alt')}}</span>
</span>
</button>
<button ngbDropdownItem (click)="read(true)">
</button>
<button ngbDropdownItem (click)="read(true)">
<span>
<i class="fa fa-book me-1" aria-hidden="true"></i>
<span class="read-btn--text">&nbsp;{{t('read')}}</span>
(<i class="fa fa-glasses ms-1" aria-hidden="true"></i>)
<span class="visually-hidden">{{t('incognito-alt')}}</span>
</span>
</button>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row g-0 mt-2" *ngIf="readingList.startingYear !== 0">
<h4 class="reading-list-years">
<ng-container *ngIf="readingList.startingMonth > 0">{{(readingList.startingMonth +'/01/2020')| date:'MMM'}}</ng-container>
<ng-container *ngIf="readingList.startingMonth > 0 && readingList.startingYear > 0">, </ng-container>
<ng-container *ngIf="readingList.startingYear > 0">{{readingList.startingYear}}</ng-container>
<ng-container *ngIf="readingList.endingYear > 0">
<ng-container *ngIf="readingList.endingMonth > 0">{{(readingList.endingMonth +'/01/2020') | date:'MMM'}}</ng-container>
<ng-container *ngIf="readingList.endingMonth > 0 && readingList.endingYear > 0">, </ng-container>
<ng-container *ngIf="readingList.endingYear > 0">{{readingList.endingYear}}</ng-container>
</ng-container>
<div class="row g-0 mt-2" *ngIf="readingList.startingYear !== 0">
<h4 class="reading-list-years">
<ng-container *ngIf="readingList.startingMonth > 0">{{(readingList.startingMonth +'/01/2020')| date:'MMM'}}</ng-container>
<ng-container *ngIf="readingList.startingMonth > 0 && readingList.startingYear > 0">, </ng-container>
<ng-container *ngIf="readingList.startingYear > 0">{{readingList.startingYear}}</ng-container>
<ng-container *ngIf="readingList.endingYear > 0">
<ng-container *ngIf="readingList.endingMonth > 0">{{(readingList.endingMonth +'/01/2020') | date:'MMM'}}</ng-container>
<ng-container *ngIf="readingList.endingMonth > 0 && readingList.endingYear > 0">, </ng-container>
<ng-container *ngIf="readingList.endingYear > 0">{{readingList.endingYear}}</ng-container>
</ng-container>
</h4>
</div>
<!-- Summary row-->
<div class="row g-0 mt-2">
<app-read-more [text]="readingListSummary" [maxLength]="250"></app-read-more>
</h4>
</div>
<!-- Summary row-->
<div class="row g-0 mt-2">
<app-read-more [text]="readingListSummary" [maxLength]="250"></app-read-more>
</div>
</div>
</div>
</div>
<ng-container *ngIf="characters$ | async as characters">
<div class="row mb-2">
<div class="row" *ngIf="characters && characters.length > 0">
<h5>{{t('characters-title')}}</h5>
<app-badge-expander [items]="characters">
<ng-template #badgeExpanderItem let-item let-position="idx">
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item" (click)="goToCharacter(item)"></app-person-badge>
</ng-template>
</app-badge-expander>
</div>
</div>
</ng-container>
<div class="row mb-1 scroll-container" #scrollingBlock>
<ng-container *ngIf="items.length === 0 && !isLoading; else loading">
<div class="mx-auto" style="width: 200px;">
{{t('no-data')}}
<ng-container *ngIf="characters$ | async as characters">
<div class="row mb-2">
<div class="row" *ngIf="characters && characters.length > 0">
<h5>{{t('characters-title')}}</h5>
<app-badge-expander [items]="characters">
<ng-template #badgeExpanderItem let-item let-position="idx">
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item" (click)="goToCharacter(item)"></app-person-badge>
</ng-template>
</app-badge-expander>
</div>
</div>
</ng-container>
<ng-template #loading>
<app-loading *ngIf="isLoading" [loading]="isLoading"></app-loading>
</ng-template>
<app-draggable-ordered-list [items]="items" (orderUpdated)="orderUpdated($event)" [accessibilityMode]="accessibilityMode"
[showRemoveButton]="false">
<ng-template #draggableItem let-item let-position="idx">
<app-reading-list-item [ngClass]="{'content-container': items.length < 100, 'non-virtualized-container': items.length >= 100}" [item]="item" [position]="position" [libraryTypes]="libraryTypes"
[promoted]="item.promoted" (read)="readChapter($event)" (remove)="itemRemoved($event, position)"></app-reading-list-item>
<div class="row mb-1 scroll-container" #scrollingBlock>
<ng-container *ngIf="items.length === 0 && !isLoading; else loading">
<div class="mx-auto" style="width: 200px;">
{{t('no-data')}}
</div>
</ng-container>
<ng-template #loading>
<app-loading *ngIf="isLoading" [loading]="isLoading"></app-loading>
</ng-template>
</app-draggable-ordered-list>
</div>
</div>
<app-draggable-ordered-list [items]="items" (orderUpdated)="orderUpdated($event)" [accessibilityMode]="accessibilityMode"
[showRemoveButton]="false">
<ng-template #draggableItem let-item let-position="idx">
<app-reading-list-item [ngClass]="{'content-container': items.length < 100, 'non-virtualized-container': items.length >= 100}" [item]="item" [position]="position" [libraryTypes]="libraryTypes"
[promoted]="item.promoted" (read)="readChapter($event)" (remove)="itemRemoved($event, position)"></app-reading-list-item>
</ng-template>
</app-draggable-ordered-list>
</div>
</div>
}
</ng-container>

View file

@ -3,7 +3,7 @@ import {ActivatedRoute, Router} from '@angular/router';
import {ToastrService} from 'ngx-toastr';
import {take} from 'rxjs/operators';
import {ConfirmService} from 'src/app/shared/confirm.service';
import {UtilityService} from 'src/app/shared/_services/utility.service';
import {Breakpoint, UtilityService} from 'src/app/shared/_services/utility.service';
import {LibraryType} from 'src/app/_models/library/library';
import {MangaFormat} from 'src/app/_models/manga-format';
import {ReadingList, ReadingListItem} from 'src/app/_models/reading-list';
@ -32,7 +32,7 @@ import {AsyncPipe, DatePipe, DecimalPipe, NgClass, NgIf} from '@angular/common';
import {
SideNavCompanionBarComponent
} from '../../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component';
import {translate, TranslocoDirective, TranslocoService} from "@ngneat/transloco";
import {translate, TranslocoDirective} from "@ngneat/transloco";
import {CardActionablesComponent} from "../../../_single-module/card-actionables/card-actionables.component";
import {FilterUtilitiesService} from "../../../shared/_services/filter-utilities.service";
import {FilterField} from "../../../_models/metadata/v2/filter-field";
@ -53,6 +53,26 @@ import {Title} from "@angular/platform-browser";
MetadataDetailComponent]
})
export class ReadingListDetailComponent implements OnInit {
private route = inject(ActivatedRoute);
private router = inject(Router);
private readingListService = inject(ReadingListService);
private actionService = inject(ActionService);
private actionFactoryService = inject(ActionFactoryService);
public utilityService = inject(UtilityService);
public imageService = inject(ImageService);
private accountService = inject(AccountService);
private toastr = inject(ToastrService);
private confirmService = inject(ConfirmService);
private libraryService = inject(LibraryService);
private readerService = inject(ReaderService);
private cdRef = inject(ChangeDetectorRef);
private filterUtilityService = inject(FilterUtilitiesService);
private titleService = inject(Title);
protected readonly MangaFormat = MangaFormat;
protected readonly Breakpoint = Breakpoint;
items: Array<ReadingListItem> = [];
listId!: number;
readingList: ReadingList | undefined;
@ -65,15 +85,8 @@ export class ReadingListDetailComponent implements OnInit {
libraryTypes: {[key: number]: LibraryType} = {};
characters$!: Observable<Person[]>;
private translocoService = inject(TranslocoService);
protected readonly MangaFormat = MangaFormat;
constructor(private route: ActivatedRoute, private router: Router, private readingListService: ReadingListService,
private actionService: ActionService, private actionFactoryService: ActionFactoryService, public utilityService: UtilityService,
public imageService: ImageService, private accountService: AccountService, private toastr: ToastrService,
private confirmService: ConfirmService, private libraryService: LibraryService, private readerService: ReaderService,
private readonly cdRef: ChangeDetectorRef, private filterUtilityService: FilterUtilitiesService, private titleService: Title) {
}
ngOnInit(): void {
const listId = this.route.snapshot.paramMap.get('id');
@ -86,6 +99,9 @@ export class ReadingListDetailComponent implements OnInit {
this.listId = parseInt(listId, 10);
this.characters$ = this.readingListService.getCharacters(this.listId);
this.accessibilityMode = this.utilityService.getActiveBreakpoint() < Breakpoint.Tablet;
this.cdRef.markForCheck();
forkJoin([
this.libraryService.getLibraries(),
this.readingListService.getReadingList(this.listId)
@ -165,10 +181,10 @@ export class ReadingListDetailComponent implements OnInit {
}
async deleteList(readingList: ReadingList) {
if (!await this.confirmService.confirm(this.translocoService.translate('toasts.confirm-delete-reading-list'))) return;
if (!await this.confirmService.confirm(translate('toasts.confirm-delete-reading-list'))) return;
this.readingListService.delete(readingList.id).subscribe(() => {
this.toastr.success(this.translocoService.translate('toasts.reading-list-deleted'));
this.toastr.success(translate('toasts.reading-list-deleted'));
this.router.navigateByUrl('/lists');
});
}
@ -186,7 +202,7 @@ export class ReadingListDetailComponent implements OnInit {
this.items.splice(position, 1);
this.items = [...this.items];
this.cdRef.markForCheck();
this.toastr.success(this.translocoService.translate('toasts.item-removed'));
this.toastr.success(translate('toasts.item-removed'));
});
}
@ -196,7 +212,7 @@ export class ReadingListDetailComponent implements OnInit {
this.cdRef.markForCheck();
this.readingListService.removeRead(this.readingList.id).subscribe((resp) => {
if (resp === 'Nothing to remove') {
this.toastr.info(this.translocoService.translate('toasts.nothing-to-remove'));
this.toastr.info(translate('toasts.nothing-to-remove'));
return;
}
this.getListItems();

View file

@ -8,7 +8,7 @@
<div class="row g-0 theme-container">
<div class="col-md-3">
<div class="col-lg-3 col-md-5 col-sm-7 col-xs-7 scroller">
<div class="pe-2">
<ul style="height: 100%" class="list-group list-group-flush">
@ -23,93 +23,95 @@
</div>
</div>
<div class="col-md-9">
@if (selectedTheme === undefined) {
<div class="col-lg-9 col-md-7 col-sm-4 col-xs-4 ps-3">
<div class="card p-3">
<div class="row pb-4">
<div class="mx-auto">
<div class="d-flex justify-content-center">
<div class="d-flex justify-content-evenly">
@if (hasAdmin$ | async) {
{{t('preview-default-admin')}}
} @else {
{{t('preview-default')}}
}
@if (selectedTheme === undefined) {
<div class="row pb-4">
<div class="mx-auto">
<div class="d-flex justify-content-center">
<div class="d-flex justify-content-evenly">
@if (hasAdmin$ | async) {
{{t('preview-default-admin')}}
} @else {
{{t('preview-default')}}
}
</div>
</div>
</div>
</div>
</div>
@if (files && files.length > 0) {
<app-loading [loading]="isUploadingTheme"></app-loading>
} @else if (hasAdmin$ | async) {
<ngx-file-drop (onFileDrop)="dropped($event)" [accept]="acceptableExtensions" [directory]="false"
dropZoneClassName="file-upload" contentClassName="file-upload-zone">
@if (files && files.length > 0) {
<app-loading [loading]="isUploadingTheme"></app-loading>
} @else if (hasAdmin$ | async) {
<ngx-file-drop (onFileDrop)="dropped($event)" [accept]="acceptableExtensions" [directory]="false"
dropZoneClassName="file-upload" contentClassName="file-upload-zone">
<ng-template ngx-file-drop-content-tmp let-openFileSelector="openFileSelector">
<div class="row g-0 mt-3 pb-3">
<div class="mx-auto">
<div class="row g-0 mb-3">
<i class="fa fa-file-upload mx-auto" style="font-size: 24px; width: 20px;" aria-hidden="true"></i>
</div>
<ng-template ngx-file-drop-content-tmp let-openFileSelector="openFileSelector">
<div class="row g-0 mt-3 pb-3">
<div class="mx-auto">
<div class="row g-0 mb-3">
<i class="fa fa-file-upload mx-auto" style="font-size: 24px; width: 20px;" aria-hidden="true"></i>
</div>
<div class="d-flex justify-content-center">
<div class="d-flex justify-content-evenly">
<span class="pe-0" href="javascript:void(0)">{{t('drag-n-drop')}}</span>
<span class="ps-1 pe-1"></span>
<a class="pe-0" href="javascript:void(0)" (click)="openFileSelector()">{{t('upload')}}<span class="phone-hidden"> {{t('upload-continued')}}</span></a>
<div class="d-flex justify-content-center">
<div class="d-flex justify-content-evenly">
<span class="pe-0" href="javascript:void(0)">{{t('drag-n-drop')}}</span>
<span class="ps-1 pe-1"></span>
<a class="pe-0" href="javascript:void(0)" (click)="openFileSelector()">{{t('upload')}}<span class="phone-hidden"> {{t('upload-continued')}}</span></a>
</div>
</div>
</div>
</div>
</div>
</ng-template>
</ng-template>
</ngx-file-drop>
}
</ngx-file-drop>
}
}
@else {
<h4>
{{selectedTheme.name | sentenceCase}}
<div class="float-end">
@if (selectedTheme.isSiteTheme) {
@if (selectedTheme.name !== 'Dark') {
<button class="btn btn-danger me-1" (click)="deleteTheme(selectedTheme.site!)">{{t('delete')}}</button>
@else {
<h4>
{{selectedTheme.name | sentenceCase}}
<div class="float-end">
@if (selectedTheme.isSiteTheme) {
@if (selectedTheme.name !== 'Dark') {
<button class="btn btn-danger me-1" (click)="deleteTheme(selectedTheme.site!)">{{t('delete')}}</button>
}
@if (hasAdmin$ | async) {
<button class="btn btn-secondary me-1" [disabled]="selectedTheme.site?.isDefault" (click)="updateDefault(selectedTheme.site!)">{{t('set-default')}}</button>
}
<button class="btn btn-primary me-1" [disabled]="currentTheme && selectedTheme.name === currentTheme.name" (click)="applyTheme(selectedTheme.site!)">{{t('apply')}}</button>
} @else {
<button class="btn btn-primary" [disabled]="selectedTheme.downloadable?.alreadyDownloaded" (click)="downloadTheme(selectedTheme.downloadable!)">{{t('download')}}</button>
}
@if (hasAdmin$ | async) {
<button class="btn btn-secondary me-1" [disabled]="selectedTheme.site?.isDefault" (click)="updateDefault(selectedTheme.site!)">{{t('set-default')}}</button>
}
<button class="btn btn-primary me-1" [disabled]="currentTheme && selectedTheme.name === currentTheme.name" (click)="applyTheme(selectedTheme.site!)">{{t('apply')}}</button>
} @else {
<button class="btn btn-primary" [disabled]="selectedTheme.downloadable?.alreadyDownloaded" (click)="downloadTheme(selectedTheme.downloadable!)">{{t('download')}}</button>
}
</div>
</h4>
@if(!selectedTheme.isSiteTheme) {
<p>{{selectedTheme.downloadable!.description | defaultValue}}</p>
</div>
</h4>
@if(!selectedTheme.isSiteTheme) {
<p>{{selectedTheme.downloadable!.description | defaultValue}}</p>
<app-carousel-reel [items]="selectedTheme.downloadable!.previewUrls" title="Preview">
<ng-template #carouselItem let-item>
<a [href]="item | safeUrl" target="_blank" rel="noopener noreferrer">
<app-image [imageUrl]="item" height="100px" width="160px"></app-image>
</a>
</ng-template>
</app-carousel-reel>
} @else {
<p>{{selectedTheme.site!.description | defaultValue}}</p>
<app-carousel-reel [items]="selectedTheme.downloadable!.previewUrls" [title]="t('preview-title')">
<ng-template #carouselItem let-item>
<a [href]="item | safeUrl" target="_blank" rel="noopener noreferrer">
<app-image [imageUrl]="item" height="108px" width="260px"></app-image>
</a>
</ng-template>
</app-carousel-reel>
} @else {
<p>{{selectedTheme.site!.description | defaultValue}}</p>
<app-carousel-reel [items]="selectedTheme.site!.previewUrls" title="Preview">
<ng-template #carouselItem let-item>
<a [href]="item | safeUrl" target="_blank" rel="noopener noreferrer">
<app-image [imageUrl]="item" height="100px" width="160px"></app-image>
</a>
</ng-template>
</app-carousel-reel>
<app-carousel-reel [items]="selectedTheme.site!.previewUrls" [title]="t('preview-title')">
<ng-template #carouselItem let-item>
<a [href]="item | safeUrl" target="_blank" rel="noopener noreferrer">
<app-image [imageUrl]="item" height="108px" width="260px"></app-image>
</a>
</ng-template>
</app-carousel-reel>
}
}
}
</div>
</div>
</div>
</div>
@ -122,12 +124,12 @@
<div class="fw-bold">{{item.name | sentenceCase}}</div>
@if (item.hasOwnProperty('provider')) {
{{item.provider | siteThemeProvider}}
<span class="pill p-1 me-1 provider">{{item.provider | siteThemeProvider}}</span>
} @else if (item.hasOwnProperty('lastCompatibleVersion')) {
{{ThemeProvider.Custom | siteThemeProvider}} • v{{item.lastCompatibleVersion}}
<span class="pill p-1 me-1 provider">{{ThemeProvider.Custom | siteThemeProvider}}</span><span class="pill p-1 me-1 version">v{{item.lastCompatibleVersion}}</span>
}
@if (currentTheme && item.name === currentTheme.name) {
• {{t('active-theme')}}
<span class="pill p-1 active">{{t('active-theme')}}</span>
}
</div>
@if (item.hasOwnProperty('isDefault') && item.isDefault) {

View file

@ -10,6 +10,25 @@
justify-content: space-around;
}
.scroller {
max-height: calc(100dvh - 280px);
overflow-y: auto;
}
.pill {
font-size: .8rem;
background-color: var(--card-bg-color);
border-radius: 0.375rem;
&.active {
background-color : var(--primary-color);
}
}
.list-group-item, .list-group-item.active {
border-top-width: 0;
border-bottom-width: 0;
}
ngx-file-drop ::ng-deep > div {
// styling for the outer drop box
width: 100%;

View file

@ -33,6 +33,7 @@
"user-scrobble-history": {
"title": "Scrobble History",
"description": "Here you will find any scrobble events linked with your account. In order for events to exist, you must have an active scrobble provider configured. All events that have been processed will clear after a month. If there are non-processed events, it is likely these cannot form matches upstream. Please reach out to your admin to get them corrected.",
"not-read-warning": "Upstream providers will always keep the highest number",
"filter-label": "{{common.filter}}",
"created-header": "Created",
"last-modified-header": "Last Modified",
@ -47,7 +48,8 @@
"rating": "Rating {{r}}",
"not-applicable": "Not Applicable",
"processed": "Processed",
"not-processed": "Not Processed"
"not-processed": "Not Processed",
"special": "{{entity-title.special}}"
},
"scrobble-event-type-pipe": {
@ -197,7 +199,8 @@
"upload": "{{cover-image-chooser.upload}}",
"upload-continued": "a css file",
"preview-default": "Select a theme first",
"preview-default-admin": "Select a theme first or upload one manually"
"preview-default-admin": "Select a theme first or upload one manually",
"preview-title": "Preview"
},
"theme": {
@ -1122,6 +1125,8 @@
"manage-email-settings": {
"title": "Email Services (SMTP)",
"description": "In order to use some functions of Kavita like Forgot Password and Send To Device, an email provider must be setup. Other features like Password change are less secure without Email setup.",
"setting-description": "You must fill out both Host Name and SMTP settings to use email-based functionality within Kavita.",
"test-warning": "You must save before using Test button.",
"send-to-warning": "If you want Send to Device to work you must setup your email settings",
"email-url-label": "Email Service URL",
"email-url-tooltip": "Use fully qualified URL of the email service. Do not include ending slash.",
@ -2199,8 +2204,8 @@
"collections-deleted": "Collections deleted",
"pdf-book-mode-screen-size": "Screen too small for Book mode",
"stack-imported": "Stack Imported",
"confirm-delete-theme": "Removing this theme will delete it from the disk. You can grab it from temp directory before removal"
"confirm-delete-theme": "Removing this theme will delete it from the disk. You can grab it from temp directory before removal",
"mal-token-required": "MAL Token is required, set in User Settings"
},
"actionable": {

View file

@ -15,6 +15,7 @@
}
$image-height: 230px;
$image-filter-height: 160px;
$image-width: 160px;
.card-item-container {
@ -62,6 +63,14 @@ $image-width: 160px;
border-top-right-radius: 4px;
z-index: 10;
&.filter {
height: $image-filter-height;
.card-overlay {
height: $image-filter-height;
}
}
&:hover {
visibility: visible;
}