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:
parent
97ffdd0975
commit
b50fa0fd1e
45 changed files with 563 additions and 282 deletions
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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:'')}}
|
||||
|
|
|
@ -42,4 +42,10 @@
|
|||
max-width: 319px;
|
||||
justify-content: space-between;
|
||||
margin: 0 auto;
|
||||
padding: .5rem 0;
|
||||
|
||||
& > * {
|
||||
margin: 0 5px;
|
||||
display: inline-flex;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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})}}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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%;
|
||||
|
|
|
@ -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%;
|
||||
|
|
|
@ -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}">
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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())
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"> {{t('remove-read')}}</span>
|
||||
</button>
|
||||
<span class="read-btn--text"> {{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"> {{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"> {{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>
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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%;
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue