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

@ -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%;