UX Pass 6 (#3131)
Co-authored-by: Joseph Milazzo <joseph.v.milazzo@gmail.com>
This commit is contained in:
parent
fc644985be
commit
62383042b0
70 changed files with 741 additions and 399 deletions
|
|
@ -7,6 +7,7 @@ import {Person} from "./metadata/person";
|
|||
import {IHasCast} from "./common/i-has-cast";
|
||||
import {IHasReadingTime} from "./common/i-has-reading-time";
|
||||
import {IHasCover} from "./common/i-has-cover";
|
||||
import {IHasProgress} from "./common/i-has-progress";
|
||||
|
||||
export const LooseLeafOrDefaultNumber = -100000;
|
||||
export const SpecialVolumeNumber = 100000;
|
||||
|
|
@ -14,7 +15,7 @@ export const SpecialVolumeNumber = 100000;
|
|||
/**
|
||||
* Chapter table object. This does not have metadata on it, use ChapterMetadata which is the same Chapter but with those fields.
|
||||
*/
|
||||
export interface Chapter extends IHasCast, IHasReadingTime, IHasCover {
|
||||
export interface Chapter extends IHasCast, IHasReadingTime, IHasCover, IHasProgress {
|
||||
id: number;
|
||||
range: string;
|
||||
/**
|
||||
|
|
|
|||
4
UI/Web/src/app/_models/common/i-has-progress.ts
Normal file
4
UI/Web/src/app/_models/common/i-has-progress.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export interface IHasProgress {
|
||||
pages: number;
|
||||
pagesRead: number;
|
||||
}
|
||||
|
|
@ -2,8 +2,9 @@ import { MangaFormat } from './manga-format';
|
|||
import { Volume } from './volume';
|
||||
import {IHasCover} from "./common/i-has-cover";
|
||||
import {IHasReadingTime} from "./common/i-has-reading-time";
|
||||
import {IHasProgress} from "./common/i-has-progress";
|
||||
|
||||
export interface Series extends IHasCover, IHasReadingTime {
|
||||
export interface Series extends IHasCover, IHasReadingTime, IHasProgress {
|
||||
id: number;
|
||||
name: string;
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@ import { Chapter } from './chapter';
|
|||
import { HourEstimateRange } from './series-detail/hour-estimate-range';
|
||||
import {IHasCover} from "./common/i-has-cover";
|
||||
import {IHasReadingTime} from "./common/i-has-reading-time";
|
||||
import {IHasProgress} from "./common/i-has-progress";
|
||||
|
||||
export interface Volume extends IHasCover, IHasReadingTime {
|
||||
export interface Volume extends IHasCover, IHasReadingTime, IHasProgress {
|
||||
id: number;
|
||||
minNumber: number;
|
||||
maxNumber: number;
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ export class NavService {
|
|||
this.renderer.setStyle(bodyElem, 'margin-top', 'var(--nav-offset)');
|
||||
this.renderer.removeStyle(bodyElem, 'scrollbar-gutter');
|
||||
this.renderer.setStyle(bodyElem, 'height', 'calc(var(--vh)*100 - var(--nav-offset))');
|
||||
this.renderer.setStyle(bodyElem, 'overflow', 'hidden');
|
||||
this.renderer.setStyle(this.document.querySelector('html'), 'height', 'calc(var(--vh)*100 - var(--nav-offset))');
|
||||
this.navbarVisibleSource.next(true);
|
||||
}, 10);
|
||||
|
|
@ -114,9 +115,10 @@ export class NavService {
|
|||
setTimeout(() => {
|
||||
const bodyElem = this.document.querySelector('body');
|
||||
this.renderer.removeStyle(bodyElem, 'height');
|
||||
this.renderer.removeStyle(this.document.querySelector('html'), 'height');
|
||||
this.renderer.setStyle(bodyElem, 'margin-top', '0px', RendererStyleFlags2.Important);
|
||||
this.renderer.setStyle(bodyElem, 'scrollbar-gutter', 'initial', RendererStyleFlags2.Important);
|
||||
this.renderer.removeStyle(this.document.querySelector('html'), 'height');
|
||||
this.renderer.setStyle(bodyElem, 'overflow', 'auto');
|
||||
this.navbarVisibleSource.next(false);
|
||||
}, 10);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
import {NgbDropdown, NgbDropdownItem, NgbDropdownMenu, NgbDropdownToggle} from '@ng-bootstrap/ng-bootstrap';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
import { Action, ActionItem } from 'src/app/_services/action-factory.service';
|
||||
import {AsyncPipe, CommonModule, NgTemplateOutlet} from "@angular/common";
|
||||
import {AsyncPipe, NgTemplateOutlet} from "@angular/common";
|
||||
import {TranslocoDirective} from "@jsverse/transloco";
|
||||
import {DynamicListPipe} from "./_pipes/dynamic-list.pipe";
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
<ng-container *transloco="let t; read: 'series-detail'">
|
||||
@if(mobileSeriesImgBackground === 'true') {
|
||||
<app-image [styles]="{'background': 'none'}" [imageUrl]="coverImage"></app-image>
|
||||
} @else {
|
||||
<app-image [styles]="{'object-fit': 'contain', 'background': 'none', 'max-height': '400px'}" [imageUrl]="coverImage"></app-image>
|
||||
}
|
||||
<div class="card-overlay"></div>
|
||||
<div class="overlay-information">
|
||||
<div class="overlay-information--centered">
|
||||
<span class="card-title library mx-auto" style="width: auto;" (click)="read.emit()">
|
||||
<!-- Card Image -->
|
||||
<div>
|
||||
<i class="fa-solid fa-book text-center" aria-hidden="true"></i>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (entity.pagesRead < entity.pages && entity.pagesRead > 0) {
|
||||
<div class="progress-banner series" ngbTooltip="{{(entity.pagesRead / entity.pages) * 100 | number:'1.0-1'}}%">
|
||||
<ngb-progressbar type="primary" [value]="entity.pagesRead" [max]="entity.pages" [showValue]="true"></ngb-progressbar>
|
||||
</div>
|
||||
@if (continueTitle !== '') {
|
||||
<div class="under-image">
|
||||
<div class="continue-from">
|
||||
{{t('continue-from', {title: continueTitle})}}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</ng-container>
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
.overlay-information {
|
||||
position: relative;
|
||||
top: -364px;
|
||||
height: 364px;
|
||||
transition: all 0.2s;
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background-color: var(--card-overlay-hover-bg-color) !important;
|
||||
|
||||
.overlay-information--centered {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.overlay-information--centered {
|
||||
position: absolute;
|
||||
border-radius: 15px;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
border-radius: 50px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 115;
|
||||
visibility: hidden;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--primary-color) !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
div {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
i {
|
||||
font-size: 1.6rem;
|
||||
line-height: 60px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.overlay-information {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 12px;
|
||||
width: calc(100% - 24px);
|
||||
height: 100%;
|
||||
transition: all 0.2s;
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--card-overlay-hover-bg-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.overlay-information--centered {
|
||||
position: absolute;
|
||||
border-radius: 15px;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
border-radius: 50px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 115;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--primary-color) !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.series {
|
||||
.overlay-information--centered {
|
||||
div {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
i {
|
||||
font-size: 1.4rem;
|
||||
line-height: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep .image-container app-image img {
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
|
||||
.progress {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.progress-banner.series {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
::ng-deep .progress-banner.series span {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: white;
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
.under-image {
|
||||
position: relative;
|
||||
|
||||
.continue-from {
|
||||
background-color: var(--breadcrumb-bg-color);
|
||||
color: white;
|
||||
border-bottom-left-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 991px) {
|
||||
.overlay-information {
|
||||
visibility: hidden;
|
||||
.overlay-information--centered {
|
||||
visibility: hidden !important;
|
||||
}
|
||||
}
|
||||
.progress-banner {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.under-image {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import {ChangeDetectionStrategy, Component, EventEmitter, Input, Output} from '@angular/core';
|
||||
import {DecimalPipe, NgClass} from "@angular/common";
|
||||
import {TranslocoDirective} from "@jsverse/transloco";
|
||||
import {ImageComponent} from "../../shared/image/image.component";
|
||||
import {NgbProgressbar, NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {IHasProgress} from "../../_models/common/i-has-progress";
|
||||
|
||||
/**
|
||||
* Used for the Series/Volume/Chapter Detail pages
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-cover-image',
|
||||
standalone: true,
|
||||
imports: [
|
||||
NgClass,
|
||||
TranslocoDirective,
|
||||
ImageComponent,
|
||||
NgbProgressbar,
|
||||
DecimalPipe,
|
||||
NgbTooltip
|
||||
],
|
||||
templateUrl: './cover-image.component.html',
|
||||
styleUrl: './cover-image.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class CoverImageComponent {
|
||||
|
||||
@Input({required: true}) coverImage!: string;
|
||||
@Input({required: true}) entity!: IHasProgress;
|
||||
@Input() continueTitle: string = '';
|
||||
@Output() read = new EventEmitter();
|
||||
|
||||
mobileSeriesImgBackground = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue('--mobile-series-img-background').trim();
|
||||
|
||||
}
|
||||
|
|
@ -1,23 +1,21 @@
|
|||
<ng-container *transloco="let t; read: 'details-tab'">
|
||||
<div class="details pb-3">
|
||||
<div class="mb-3">
|
||||
<app-carousel-reel [items]="genres" [title]="t('genres-title')">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-tag-badge (click)="openGeneric(FilterField.Genres, item.id)" [selectionMode]="TagBadgeCursor.Clickable">
|
||||
{{item.title}}
|
||||
</app-tag-badge>
|
||||
<h4 class="header">{{t('genres-title')}}</h4>
|
||||
<app-badge-expander [includeComma]="true" [items]="genres">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx" let-last="last">
|
||||
<a href="javascript:void(0)" class="dark-exempt btn-icon" (click)="openGeneric(FilterField.Genres, item.id)">{{item.title}}</a>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<app-carousel-reel [items]="tags" [title]="t('tags-title')">
|
||||
<ng-template #carouselItem let-item>
|
||||
<app-tag-badge (click)="openGeneric(FilterField.Tags, item.id)" [selectionMode]="TagBadgeCursor.Clickable">
|
||||
{{item.title}}
|
||||
</app-tag-badge>
|
||||
<h4 class="header">{{t('tags-title')}}</h4>
|
||||
<app-badge-expander [includeComma]="true" [items]="tags">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx" let-last="last">
|
||||
<a href="javascript:void(0)" class="dark-exempt btn-icon" (click)="openGeneric(FilterField.Tags, item.id)">{{item.title}}</a>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
|
|
|
|||
|
|
@ -14,18 +14,20 @@ import {TagBadgeComponent, TagBadgeCursor} from "../../shared/tag-badge/tag-badg
|
|||
import {ImageComponent} from "../../shared/image/image.component";
|
||||
import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe";
|
||||
import {ImageService} from "../../_services/image.service";
|
||||
import {BadgeExpanderComponent} from "../../shared/badge-expander/badge-expander.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-details-tab',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CarouselReelComponent,
|
||||
PersonBadgeComponent,
|
||||
TranslocoDirective,
|
||||
TagBadgeComponent,
|
||||
ImageComponent,
|
||||
SafeHtmlPipe
|
||||
],
|
||||
imports: [
|
||||
CarouselReelComponent,
|
||||
PersonBadgeComponent,
|
||||
TranslocoDirective,
|
||||
TagBadgeComponent,
|
||||
ImageComponent,
|
||||
SafeHtmlPipe,
|
||||
BadgeExpanderComponent
|
||||
],
|
||||
templateUrl: './details-tab.component.html',
|
||||
styleUrl: './details-tab.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
|
|
|
|||
|
|
@ -553,15 +553,17 @@
|
|||
</div>
|
||||
}
|
||||
|
||||
<app-setting-item [title]="t('files-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||
<ng-template #view>
|
||||
@for (file of chapter.files; track file.id) {
|
||||
<div>
|
||||
<span>{{file.filePath}}</span><span class="ms-2 me-2">•</span><span>{{file.bytes | bytes}}</span>
|
||||
</div>
|
||||
}
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
@if (accountService.isAdmin$ | async) {
|
||||
<app-setting-item [title]="t('files-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||
<ng-template #view>
|
||||
@for (file of chapter.files; track file.id) {
|
||||
<div>
|
||||
<span>{{file.filePath}}</span><span class="ms-2 me-2">•</span><span>{{file.bytes | bytes}}</span>
|
||||
</div>
|
||||
}
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
}
|
||||
|
||||
</ng-template>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ import {Language} from "../../_models/metadata/language";
|
|||
import {Person, PersonRole} from "../../_models/metadata/person";
|
||||
import {Genre} from "../../_models/metadata/genre";
|
||||
import {AgeRatingDto} from "../../_models/metadata/age-rating-dto";
|
||||
import {SeriesService} from "../../_services/series.service";
|
||||
import {ImageService} from "../../_services/image.service";
|
||||
import {UploadService} from "../../_services/upload.service";
|
||||
import {MetadataService} from "../../_services/metadata.service";
|
||||
|
|
|
|||
|
|
@ -88,16 +88,17 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<app-setting-item [title]="t('files-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||
<ng-template #view>
|
||||
@for (file of files; track file.id) {
|
||||
<div>
|
||||
<span>{{file.filePath}}</span><span class="ms-2 me-2">•</span><span>{{file.bytes | bytes}}</span>
|
||||
</div>
|
||||
}
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
@if (accountService.isAdmin$ | async) {
|
||||
<app-setting-item [title]="t('files-label')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||
<ng-template #view>
|
||||
@for (file of files; track file.id) {
|
||||
<div>
|
||||
<span>{{file.filePath}}</span><span class="ms-2 me-2">•</span><span>{{file.bytes | bytes}}</span>
|
||||
</div>
|
||||
}
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
}
|
||||
|
||||
</ng-template>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {
|
|||
ViewContainerRef,
|
||||
ViewEncapsulation
|
||||
} from '@angular/core';
|
||||
import {CommonModule, DOCUMENT, NgOptimizedImage} from '@angular/common';
|
||||
import {DOCUMENT, NgOptimizedImage} from '@angular/common';
|
||||
import {NgbActiveModal} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {ReactiveFormsModule} from "@angular/forms";
|
||||
import {UserReview} from "../review-card/user-review";
|
||||
|
|
@ -20,7 +20,7 @@ import {ProviderImagePipe} from "../../_pipes/provider-image.pipe";
|
|||
@Component({
|
||||
selector: 'app-review-card-modal',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ReactiveFormsModule, SpoilerComponent, SafeHtmlPipe, TranslocoDirective, DefaultValuePipe, NgOptimizedImage, ProviderImagePipe],
|
||||
imports: [ReactiveFormsModule, SpoilerComponent, SafeHtmlPipe, TranslocoDirective, DefaultValuePipe, NgOptimizedImage, ProviderImagePipe],
|
||||
templateUrl: './review-card-modal.component.html',
|
||||
styleUrls: ['./review-card-modal.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
|
|
|||
|
|
@ -8,13 +8,12 @@ import {
|
|||
OnInit,
|
||||
Output
|
||||
} from '@angular/core';
|
||||
import {CommonModule, NgOptimizedImage} from '@angular/common';
|
||||
import {NgOptimizedImage} from '@angular/common';
|
||||
import {UserReview} from "./user-review";
|
||||
import {NgbModal} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {ReviewCardModalComponent} from "../review-card-modal/review-card-modal.component";
|
||||
import {AccountService} from "../../_services/account.service";
|
||||
import {
|
||||
ReviewSeriesModalCloseAction,
|
||||
ReviewSeriesModalCloseEvent,
|
||||
ReviewSeriesModalComponent
|
||||
} from "../review-series-modal/review-series-modal.component";
|
||||
|
|
|
|||
|
|
@ -13,14 +13,16 @@
|
|||
<textarea id="review" class="form-control" formControlName="reviewBody" rows="3" [minlength]="minLength"
|
||||
[class.is-invalid]="reviewGroup.get('reviewBody')?.invalid && reviewGroup.get('reviewBody')?.touched" aria-describedby="body-validations"
|
||||
></textarea>
|
||||
<div id="body-validations" class="invalid-feedback" *ngIf="reviewGroup.dirty || reviewGroup.touched">
|
||||
@if (reviewGroup.get('reviewBody')?.errors?.required) {
|
||||
<div>{{t('required')}}</div>
|
||||
}
|
||||
@if (reviewGroup.get('reviewBody')?.errors?.minlength) {
|
||||
<div>{{t('min-length', {count: minLength})}}</div>
|
||||
}
|
||||
</div>
|
||||
@if (reviewGroup.dirty || reviewGroup.touched) {
|
||||
<div id="body-validations" class="invalid-feedback">
|
||||
@if (reviewGroup.get('reviewBody')?.errors?.required) {
|
||||
<div>{{t('required')}}</div>
|
||||
}
|
||||
@if (reviewGroup.get('reviewBody')?.errors?.minlength) {
|
||||
<div>{{t('min-length', {count: minLength})}}</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import {
|
|||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
EventEmitter,
|
||||
inject,
|
||||
Input,
|
||||
OnInit
|
||||
|
|
@ -11,7 +10,6 @@ import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/
|
|||
import {NgbActiveModal, NgbRating} from '@ng-bootstrap/ng-bootstrap';
|
||||
import { SeriesService } from 'src/app/_services/series.service';
|
||||
import {UserReview} from "../review-card/user-review";
|
||||
import {CommonModule} from "@angular/common";
|
||||
import {translate, TranslocoDirective} from "@jsverse/transloco";
|
||||
import {ConfirmService} from "../../shared/confirm.service";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
|
|
@ -31,7 +29,7 @@ export interface ReviewSeriesModalCloseEvent {
|
|||
@Component({
|
||||
selector: 'app-review-series-modal',
|
||||
standalone: true,
|
||||
imports: [CommonModule, NgbRating, ReactiveFormsModule, TranslocoDirective],
|
||||
imports: [NgbRating, ReactiveFormsModule, TranslocoDirective],
|
||||
templateUrl: './review-series-modal.component.html',
|
||||
styleUrls: ['./review-series-modal.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
|
|
|
|||
|
|
@ -22,6 +22,6 @@ a.read-more-link {
|
|||
}
|
||||
|
||||
.offcanvas-body {
|
||||
mask-image: linear-gradient(to bottom, transparent, black 0%, black 95%, transparent 100%);
|
||||
-webkit-mask-image: linear-gradient(to bottom, transparent, black 0%, black 95%, transparent 100%);
|
||||
mask-image: linear-gradient(to bottom, transparent, black 0%, black 97%, transparent 100%);
|
||||
-webkit-mask-image: linear-gradient(to bottom, transparent, black 0%, black 97%, transparent 100%);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
<ng-container *transloco="let t; read:'spoiler'">
|
||||
<div (click)="toggle()" [attr.aria-expanded]="!isCollapsed" class="btn spoiler" tabindex="0">
|
||||
<span *ngIf="isCollapsed; else show">{{t('click-to-show')}}</span>
|
||||
<ng-template #show>
|
||||
@if (isCollapsed) {
|
||||
<span>{{t('click-to-show')}}</span>
|
||||
} @else {
|
||||
<div [innerHTML]="html | safeHtml"></div>
|
||||
</ng-template>
|
||||
}
|
||||
</div>
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -7,14 +7,13 @@ import {
|
|||
OnInit,
|
||||
ViewEncapsulation
|
||||
} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe";
|
||||
import {TranslocoDirective} from "@jsverse/transloco";
|
||||
|
||||
@Component({
|
||||
selector: 'app-spoiler',
|
||||
standalone: true,
|
||||
imports: [CommonModule, SafeHtmlPipe, TranslocoDirective],
|
||||
imports: [SafeHtmlPipe, TranslocoDirective],
|
||||
templateUrl: './spoiler.component.html',
|
||||
styleUrls: ['./spoiler.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, OnInit} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
|
||||
import {ScrobbleProvider, ScrobblingService} from "../../_services/scrobbling.service";
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
|
|
@ -21,7 +20,7 @@ import {LooseLeafOrDefaultNumber, SpecialVolumeNumber} from "../../_models/chapt
|
|||
@Component({
|
||||
selector: 'app-user-scrobble-history',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ScrobbleEventTypePipe, NgbPagination, ReactiveFormsModule, SortableHeader, TranslocoModule,
|
||||
imports: [ScrobbleEventTypePipe, NgbPagination, ReactiveFormsModule, SortableHeader, TranslocoModule,
|
||||
DefaultValuePipe, TranslocoLocaleModule, UtcToLocalTimePipe, NgbTooltip],
|
||||
templateUrl: './user-scrobble-history.component.html',
|
||||
styleUrls: ['./user-scrobble-history.component.scss'],
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
<app-image [imageUrl]="imageUrl" height="32px" width="32px" ngbTooltip="{{rating | ageRating}}"></app-image>
|
||||
<app-image [imageUrl]="imageUrl" height="32px" width="32px" classes="clickable" ngbTooltip="{{rating | ageRating}}" (click)="openRating()"></app-image>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ import {ImageComponent} from "../../shared/image/image.component";
|
|||
import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {AgeRatingPipe} from "../../_pipes/age-rating.pipe";
|
||||
import {AsyncPipe} from "@angular/common";
|
||||
import {FilterUtilitiesService} from "../../shared/_services/filter-utilities.service";
|
||||
import {FilterComparison} from "../../_models/metadata/v2/filter-comparison";
|
||||
import {FilterField} from "../../_models/metadata/v2/filter-field";
|
||||
|
||||
const basePath = './assets/images/ratings/';
|
||||
|
||||
|
|
@ -22,9 +25,11 @@ const basePath = './assets/images/ratings/';
|
|||
})
|
||||
export class AgeRatingImageComponent implements OnInit {
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly filterUtilityService = inject(FilterUtilitiesService);
|
||||
|
||||
protected readonly AgeRating = AgeRating;
|
||||
|
||||
@Input({required: true}) rating: AgeRating = AgeRating.Unknown;
|
||||
protected readonly AgeRating = AgeRating;
|
||||
|
||||
imageUrl: string = 'unknown-rating.png';
|
||||
|
||||
|
|
@ -79,4 +84,9 @@ export class AgeRatingImageComponent implements OnInit {
|
|||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
openRating() {
|
||||
this.filterUtilityService.applyFilter(['all-series'], FilterField.AgeRating, FilterComparison.Equal, `${this.rating}`).subscribe();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
<ng-container *transloco="let t; read: 'manage-email-settings'">
|
||||
<p>{{t('description')}}</p>
|
||||
|
||||
<form [formGroup]="settingsForm">
|
||||
<p class="alert alert-warning">{{t('setting-description')}}</p>
|
||||
|
||||
|
|
|
|||
|
|
@ -24,10 +24,6 @@
|
|||
</select>
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
|
||||
@if (formControl.dirty) {
|
||||
<div class="alert alert-danger mt-2" role="alert">{{t('media-warning')}}</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -75,6 +75,12 @@ export class ManageMediaSettingsComponent implements OnInit {
|
|||
return this.settingsService.updateServerSettings(data);
|
||||
}),
|
||||
tap(settings => {
|
||||
|
||||
const encodingChanged = this.serverSettings.encodeMediaAs !== settings.encodeMediaAs;
|
||||
if (encodingChanged) {
|
||||
this.toastr.info(translate('manage-media-settings.media-warning'));
|
||||
}
|
||||
|
||||
this.serverSettings = settings;
|
||||
this.resetForm();
|
||||
this.cdRef.markForCheck();
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import {
|
|||
QueryList,
|
||||
ViewChildren
|
||||
} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {FormControl, FormGroup, ReactiveFormsModule} from "@angular/forms";
|
||||
import {compare, SortableHeader, SortEvent} from "../../_single-module/table/_directives/sortable-header.directive";
|
||||
import {KavitaMediaError} from "../_models/media-error";
|
||||
|
|
@ -34,7 +33,7 @@ import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe";
|
|||
@Component({
|
||||
selector: 'app-manage-scrobble-errors',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ReactiveFormsModule, FilterPipe, LoadingComponent, SortableHeader, TranslocoModule, DefaultDatePipe, DefaultValuePipe, TranslocoLocaleModule, UtcToLocalTimePipe],
|
||||
imports: [ReactiveFormsModule, FilterPipe, LoadingComponent, SortableHeader, TranslocoModule, DefaultDatePipe, DefaultValuePipe, TranslocoLocaleModule, UtcToLocalTimePipe],
|
||||
templateUrl: './manage-scrobble-errors.component.html',
|
||||
styleUrls: ['./manage-scrobble-errors.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import {SettingItemComponent} from "../../settings/_components/setting-item/sett
|
|||
import {SettingSwitchComponent} from "../../settings/_components/setting-switch/setting-switch.component";
|
||||
import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe";
|
||||
import {ConfirmService} from "../../shared/confirm.service";
|
||||
import {debounceTime, distinctUntilChanged, filter, switchMap, tap} from "rxjs";
|
||||
import {debounceTime, distinctUntilChanged, filter, of, switchMap, tap} from "rxjs";
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
|
||||
const ValidIpAddress = /^(\s*((([12]?\d{1,2}\.){3}[12]?\d{1,2})|(([\da-f]{0,4}\:){0,7}([\da-f]{0,4})))\s*\,)*\s*((([12]?\d{1,2}\.){3}[12]?\d{1,2})|(([\da-f]{0,4}\:){0,7}([\da-f]{0,4})))\s*$/i;
|
||||
|
|
@ -69,7 +69,6 @@ export class ManageSettingsComponent implements OnInit {
|
|||
this.settingsForm.addControl('allowStatCollection', new FormControl(this.serverSettings.allowStatCollection, [Validators.required]));
|
||||
this.settingsForm.addControl('enableOpds', new FormControl(this.serverSettings.enableOpds, [Validators.required]));
|
||||
this.settingsForm.addControl('baseUrl', new FormControl(this.serverSettings.baseUrl, [Validators.pattern(/^(\/[\w-]+)*\/$/)]));
|
||||
this.settingsForm.addControl('emailServiceUrl', new FormControl(this.serverSettings.emailServiceUrl, [Validators.required]));
|
||||
this.settingsForm.addControl('totalBackups', new FormControl(this.serverSettings.totalBackups, [Validators.required, Validators.min(1), Validators.max(30)]));
|
||||
this.settingsForm.addControl('cacheSize', new FormControl(this.serverSettings.cacheSize, [Validators.required, Validators.min(50)]));
|
||||
this.settingsForm.addControl('totalLogs', new FormControl(this.serverSettings.totalLogs, [Validators.required, Validators.min(1), Validators.max(30)]));
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ export class ManageTasksSettingsComponent implements OnInit {
|
|||
api: defer(() => {
|
||||
localStorage.removeItem('@transloco/translations/timestamp');
|
||||
localStorage.removeItem('@transloco/translations');
|
||||
localStorage.removeItem('translocoLang');
|
||||
location.reload();
|
||||
return of();
|
||||
}),
|
||||
successMessage: 'bust-locale-task-success',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnInit} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {JumpKey} from "../_models/jumpbar/jump-key";
|
||||
import {TranslocoDirective} from "@jsverse/transloco";
|
||||
import {CardItemComponent} from "../cards/card-item/card-item.component";
|
||||
|
|
@ -19,11 +18,12 @@ import {ActionService} from "../_services/action.service";
|
|||
import {FilterPipe} from "../_pipes/filter.pipe";
|
||||
import {filter} from "rxjs";
|
||||
import {ManageSmartFiltersComponent} from "../sidenav/_components/manage-smart-filters/manage-smart-filters.component";
|
||||
import {DecimalPipe} from "@angular/common";
|
||||
|
||||
@Component({
|
||||
selector: 'app-all-filters',
|
||||
standalone: true,
|
||||
imports: [CommonModule, TranslocoDirective, CardItemComponent, SideNavCompanionBarComponent, CardDetailLayoutComponent, SafeHtmlPipe, CardActionablesComponent, RouterLink, FilterPipe, ManageSmartFiltersComponent],
|
||||
imports: [TranslocoDirective, CardItemComponent, SideNavCompanionBarComponent, CardDetailLayoutComponent, SafeHtmlPipe, CardActionablesComponent, RouterLink, FilterPipe, ManageSmartFiltersComponent, DecimalPipe],
|
||||
templateUrl: './all-filters.component.html',
|
||||
styleUrl: './all-filters.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
|
|
@ -40,7 +40,6 @@ export class AllFiltersComponent implements OnInit {
|
|||
jumpbarKeys: Array<JumpKey> = [];
|
||||
filters: SmartFilter[] = [];
|
||||
isLoading = true;
|
||||
trackByIdentity = (index: number, item: SmartFilter) => item.name;
|
||||
|
||||
ngOnInit() {
|
||||
this.loadData();
|
||||
|
|
@ -55,14 +54,6 @@ export class AllFiltersComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
async loadSmartFilter(filter: SmartFilter) {
|
||||
await this.router.navigateByUrl('all-series?' + filter.filter);
|
||||
}
|
||||
|
||||
isErrored(filter: SmartFilter) {
|
||||
return !decodeURIComponent(filter.filter).includes('¦');
|
||||
}
|
||||
|
||||
async deleteFilter(filter: SmartFilter) {
|
||||
await this.actionService.deleteFilter(filter.id, success => {
|
||||
this.filters = this.filters.filter(f => f.id != filter.id);
|
||||
|
|
@ -80,6 +71,4 @@ export class AllFiltersComponent implements OnInit {
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected readonly filter = filter;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
@import '../theme/variables';
|
||||
.content-wrapper {
|
||||
padding: 0 10px 0;
|
||||
padding: 0 0 0 10px;
|
||||
height: calc(var(--vh)* 100 - var(--nav-offset));
|
||||
}
|
||||
|
||||
|
|
@ -7,23 +8,44 @@
|
|||
.companion-bar {
|
||||
transition: all var(--side-nav-companion-bar-transistion);
|
||||
margin-left: 40px;
|
||||
overflow-y: auto;
|
||||
overflow-y: hidden;
|
||||
overflow-x: hidden;
|
||||
height: calc(var(--vh)* 100 - var(--nav-offset));
|
||||
width: 100%;
|
||||
height: calc(var(--vh)* 100 - var(--nav-mobile-offset));
|
||||
padding-right: 10px;
|
||||
scrollbar-gutter: stable both-edges;
|
||||
scrollbar-width: thin;
|
||||
mask-image: linear-gradient(to bottom, transparent, black 0%, black 95%, transparent 100%);
|
||||
-webkit-mask-image: linear-gradient(to bottom, transparent, black 0%, black 95%, transparent 100%);
|
||||
|
||||
// For firefox
|
||||
@supports (-moz-appearance:none) {
|
||||
scrollbar-color: transparent transparent;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
background-color: transparent; /*make scrollbar space invisible */
|
||||
width: inherit;
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: transparent; /*makes it invisible when not hovering*/
|
||||
background: transparent; /*makes it invisible when not hovering*/
|
||||
}
|
||||
|
||||
&:hover {
|
||||
scrollbar-width: thin;
|
||||
overflow-y: auto;
|
||||
|
||||
// For firefox
|
||||
@supports (-moz-appearance:none) {
|
||||
scrollbar-color: rgba(255,255,255,0.3) rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
visibility: visible;
|
||||
background-color: rgba(255,255,255,0.3); /*On hover, it will turn grey*/
|
||||
}
|
||||
}
|
||||
|
|
@ -36,12 +58,10 @@
|
|||
|
||||
.companion-bar-content {
|
||||
margin-left: 190px;
|
||||
mask-image: linear-gradient(to bottom, transparent, black 0%, black 96%, transparent 100%);
|
||||
-webkit-mask-image: linear-gradient(to bottom, transparent, black 0%, black 96%, transparent 100%);
|
||||
width: calc(100% - 190px);
|
||||
width: calc(100% - 180px);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@media (max-width: $grid-breakpoints-lg) {
|
||||
::ng-deep html {
|
||||
height: 100dvh !important;
|
||||
}
|
||||
|
|
@ -52,34 +72,24 @@
|
|||
|
||||
.content-wrapper {
|
||||
overflow: hidden;
|
||||
height: calc(var(--vh)* 100 - var(--nav-mobile-offset));
|
||||
height: calc(var(--vh)* 100);
|
||||
padding: 0 10px 0;
|
||||
|
||||
&.closed {
|
||||
overflow: auto;
|
||||
height: calc(var(--vh) * 100);
|
||||
}
|
||||
}
|
||||
|
||||
.companion-bar {
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
width: calc(100vw - 30px);
|
||||
padding-top: 20px;
|
||||
height: calc(100dvh - var(--nav-mobile-offset));
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: inherit;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: transparent; /*makes it invisible when not hovering*/
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(255,255,255,0.3); /*On hover, it will turn grey*/
|
||||
}
|
||||
}
|
||||
width: 100%;
|
||||
padding: 10px 0 0;
|
||||
height: calc(var(--vh) * 100 - var(--nav-mobile-offset));
|
||||
scrollbar-color: rgba(255,255,255,0.3) rgba(0, 0, 0, 0.1);
|
||||
scrollbar-width: thin;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.companion-bar-content {
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@
|
|||
--accordion-header-bg-color: grey;
|
||||
--br-actionbar-button-hover-border-color: #6c757d;
|
||||
--br-actionbar-bg-color: white;
|
||||
--default-state-scrollbar: var(--primary-color-scrollbar);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -53,7 +54,22 @@ $dark-form-background-no-opacity: rgb(1, 4, 9);
|
|||
$primary-color: #0062cc;
|
||||
$action-bar-height: 38px;
|
||||
|
||||
.reader-container {
|
||||
&::-webkit-scrollbar {
|
||||
background-color: transparent; /*make scrollbar space invisible */
|
||||
width: inherit;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: var(--default-state-scrollbar); /*makes it invisible when not hovering*/
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: var(--primary-color-scrollbar); /*On hover, it will turn grey*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Drawer
|
||||
.control-container {
|
||||
|
|
|
|||
|
|
@ -1,24 +1,32 @@
|
|||
<ng-container *transloco="let t; read: 'personal-table-of-contents'">
|
||||
<div class="table-of-contents">
|
||||
<div *ngIf="Pages.length === 0">
|
||||
<em>{{t('no-data')}}</em>
|
||||
</div>
|
||||
@if (Pages.length === 0) {
|
||||
<div>
|
||||
<em>{{t('no-data')}}</em>
|
||||
</div>
|
||||
}
|
||||
<ul>
|
||||
<li *ngFor="let page of Pages">
|
||||
<span (click)="loadChapterPage(page, '')">{{t('page', {value: page})}}</span>
|
||||
<ul class="chapter-title">
|
||||
<li class="ellipsis"
|
||||
[ngbTooltip]="bookmark.title"
|
||||
placement="right"
|
||||
*ngFor="let bookmark of bookmarks[page]" (click)="loadChapterPage(bookmark.pageNumber, bookmark.bookScrollId); $event.stopPropagation();">
|
||||
{{bookmark.title}}
|
||||
<button class="btn btn-icon ms-1" (click)="removeBookmark(bookmark); $event.stopPropagation();">
|
||||
<i class="fa-solid fa-trash" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{t('delete', {bookmarkName: bookmark.title})}}</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
@for (page of Pages; track page) {
|
||||
<li>
|
||||
<span (click)="loadChapterPage(page, '')">{{t('page', {value: page})}}</span>
|
||||
<ul class="chapter-title">
|
||||
@for(bookmark of bookmarks[page]; track bookmark) {
|
||||
<li class="ellipsis"
|
||||
[ngbTooltip]="bookmark.title"
|
||||
placement="right"
|
||||
(click)="loadChapterPage(bookmark.pageNumber, bookmark.bookScrollId); $event.stopPropagation();">
|
||||
{{bookmark.title}}
|
||||
<button class="btn btn-icon ms-1" (click)="removeBookmark(bookmark); $event.stopPropagation();">
|
||||
<i class="fa-solid fa-trash" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{t('delete', {bookmarkName: bookmark.title})}}</span>
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
|
||||
</ul>
|
||||
</li>
|
||||
}
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
OnInit,
|
||||
Output
|
||||
} from '@angular/core';
|
||||
import {CommonModule, DOCUMENT} from '@angular/common';
|
||||
import {DOCUMENT} from '@angular/common';
|
||||
import {ReaderService} from "../../../_services/reader.service";
|
||||
import {PersonalToC} from "../../../_models/readers/personal-toc";
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
|
|
@ -23,7 +23,7 @@ export interface PersonalToCEvent {
|
|||
@Component({
|
||||
selector: 'app-personal-table-of-contents',
|
||||
standalone: true,
|
||||
imports: [CommonModule, NgbTooltip, TranslocoDirective],
|
||||
imports: [NgbTooltip, TranslocoDirective],
|
||||
templateUrl: './personal-table-of-contents.component.html',
|
||||
styleUrls: ['./personal-table-of-contents.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
|
|
|
|||
|
|
@ -647,7 +647,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<h4>Volumes</h4>
|
||||
<h4>{{t('volumes-title')}}</h4>
|
||||
@if (isLoadingVolumes) {
|
||||
<div class="spinner-border text-secondary" role="status">
|
||||
<span class="visually-hidden">{{t('loading')}}</span>
|
||||
|
|
@ -671,7 +671,7 @@
|
|||
<div class="row g-0">
|
||||
<div class="col">
|
||||
<button type="button" class="btn btn-outline-primary" (click)="collapse.toggle()"
|
||||
[attr.aria-expanded]="!volumeCollapsed[volume.name]">
|
||||
[attr.aria-expanded]="!volumeCollapsed[volume.name]" [disabled]="!isAdmin">
|
||||
{{t('view-files')}}
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import {
|
|||
NgbNavOutlet,
|
||||
NgbTooltip
|
||||
} from '@ng-bootstrap/ng-bootstrap';
|
||||
import { forkJoin, Observable, of } from 'rxjs';
|
||||
import {forkJoin, Observable, of, tap} from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
import { TypeaheadSettings } from 'src/app/typeahead/_models/typeahead-settings';
|
||||
|
|
@ -184,6 +184,7 @@ export class EditSeriesModalComponent implements OnInit {
|
|||
*/
|
||||
selectedCover: string = '';
|
||||
coverImageReset = false;
|
||||
isAdmin: boolean = false;
|
||||
|
||||
saveNestedComponents: EventEmitter<void> = new EventEmitter();
|
||||
|
||||
|
|
@ -202,6 +203,11 @@ export class EditSeriesModalComponent implements OnInit {
|
|||
this.libraryName = names[this.series.libraryId];
|
||||
});
|
||||
|
||||
this.accountService.isAdmin$.pipe(takeUntilDestroyed(this.destroyRef), tap(isAdmin => {
|
||||
this.isAdmin = isAdmin;
|
||||
this.cdRef.markForCheck();
|
||||
})).subscribe();
|
||||
|
||||
this.initSeries = Object.assign({}, this.series);
|
||||
|
||||
this.editSeriesForm = this.fb.group({
|
||||
|
|
|
|||
|
|
@ -1,33 +1,36 @@
|
|||
<ng-container *transloco="let t; read: 'bulk-operations'">
|
||||
<ng-container *ngIf="bulkSelectionService.selections$ | async as selectionCount">
|
||||
<div *ngIf="selectionCount > 0" class="bulk-select mb-3 {{modalMode ? '' : 'fixed-top'}}" [ngStyle]="{'margin-top': topOffset + 'px'}">
|
||||
<div class="d-flex justify-content-around align-items-center">
|
||||
@if (bulkSelectionService.selections$ | async; as selectionCount) {
|
||||
@if (selectionCount > 0) {
|
||||
<div class="bulk-select mb-3 {{modalMode ? '' : 'fixed-top'}}" [ngStyle]="{'margin-top': topOffset + 'px'}">
|
||||
<div class="d-flex justify-content-around align-items-center">
|
||||
|
||||
<span class="highlight">
|
||||
<i class="fa fa-check me-1" aria-hidden="true"></i>
|
||||
{{t('items-selected',{num: selectionCount | number})}}
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<span>
|
||||
@if (hasMarkAsUnread) {
|
||||
<button class="btn btn-icon" (click)="executeAction(Action.MarkAsUnread)" [ngbTooltip]="t('mark-as-unread')" placement="bottom">
|
||||
<i class="fa-regular fa-circle-check" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{t('mark-as-unread')}}</span>
|
||||
</button>
|
||||
}
|
||||
@if (hasMarkAsRead) {
|
||||
<button class="btn btn-icon" (click)="executeAction(Action.MarkAsRead)" [ngbTooltip]="t('mark-as-read')" placement="bottom">
|
||||
@if (hasMarkAsRead) {
|
||||
<button class="btn btn-icon" (click)="executeAction(Action.MarkAsRead)" [ngbTooltip]="t('mark-as-read')" placement="bottom">
|
||||
<i class="fa-solid fa-circle-check" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{t('mark-as-read')}}</span>
|
||||
</button>
|
||||
}
|
||||
<app-card-actionables [actions]="actions" labelBy="bulk-actions-header" iconClass="fa-ellipsis-h" (actionHandler)="performAction($event)"></app-card-actionables>
|
||||
}
|
||||
<app-card-actionables [actions]="actions" labelBy="bulk-actions-header" iconClass="fa-ellipsis-h" (actionHandler)="performAction($event)"></app-card-actionables>
|
||||
</span>
|
||||
|
||||
<span id="bulk-actions-header" class="visually-hidden">Bulk Actions</span>
|
||||
<span id="bulk-actions-header" class="visually-hidden">Bulk Actions</span>
|
||||
|
||||
<button class="btn btn-icon" (click)="bulkSelectionService.deselectAll()"><i class="fa fa-times me-1" aria-hidden="true"></i>{{t('deselect-all')}}</button>
|
||||
<button class="btn btn-icon" (click)="bulkSelectionService.deselectAll()"><i class="fa fa-times me-1" aria-hidden="true"></i>{{t('deselect-all')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
}
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {
|
|||
import { Action, ActionFactoryService, ActionItem } from 'src/app/_services/action-factory.service';
|
||||
import { BulkSelectionService } from '../bulk-selection.service';
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import {AsyncPipe, CommonModule} from "@angular/common";
|
||||
import {AsyncPipe, DecimalPipe, NgStyle} from "@angular/common";
|
||||
import {TranslocoModule} from "@jsverse/transloco";
|
||||
import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {CardActionablesComponent} from "../../_single-module/card-actionables/card-actionables.component";
|
||||
|
|
@ -19,11 +19,12 @@ import {CardActionablesComponent} from "../../_single-module/card-actionables/ca
|
|||
selector: 'app-bulk-operations',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
AsyncPipe,
|
||||
CardActionablesComponent,
|
||||
TranslocoModule,
|
||||
NgbTooltip
|
||||
NgbTooltip,
|
||||
NgStyle,
|
||||
DecimalPipe
|
||||
],
|
||||
templateUrl: './bulk-operations.component.html',
|
||||
styleUrls: ['./bulk-operations.component.scss'],
|
||||
|
|
@ -36,7 +37,7 @@ export class BulkOperationsComponent implements OnInit {
|
|||
* Modal mode means don't fix to the top
|
||||
*/
|
||||
@Input() modalMode = false;
|
||||
@Input() topOffset: number = 60;
|
||||
@Input() topOffset: number = 75;
|
||||
hasMarkAsRead: boolean = false;
|
||||
hasMarkAsUnread: boolean = false;
|
||||
actions: Array<ActionItem<any>> = [];
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
<app-metadata-filter [filterSettings]="filterSettings" [filterOpen]="filterOpen" (applyFilter)="applyMetadataFilter($event)"></app-metadata-filter>
|
||||
<div class="viewport-container ms-1" [ngClass]="{'empty': items.length === 0 && !isLoading}">
|
||||
<div class="content-container">
|
||||
<div class="card-container mt-2 mb-2">
|
||||
<div class="card-container mt-">
|
||||
@if (items.length === 0 && !isLoading) {
|
||||
<p><ng-container [ngTemplateOutlet]="noDataTemplate"></ng-container></p>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
height: calc((var(--vh) *100) - 173px);
|
||||
height: calc((var(--vh) *100) - 157px);
|
||||
margin-bottom: 10px;
|
||||
|
||||
&.empty {
|
||||
|
|
@ -94,10 +94,12 @@
|
|||
|
||||
.virtual-scroller, virtual-scroller {
|
||||
width: 100%;
|
||||
height: calc(var(--vh) * 100 - 143px);
|
||||
mask-image: linear-gradient(to bottom, transparent, black 0%, black 95%, transparent 100%);
|
||||
-webkit-mask-image: linear-gradient(to bottom, transparent, black 0%, black 95%, transparent 100%);
|
||||
height: calc(var(--vh) * 100 - 155px);
|
||||
mask-image: linear-gradient(to bottom, transparent, black 0%, black 97%, transparent 100%);
|
||||
-webkit-mask-image: linear-gradient(to bottom, transparent, black 0%, black 97%, transparent 100%);
|
||||
overflow: auto;
|
||||
scrollbar-color: rgba(255,255,255,0.3) rgba(0, 0, 0, 0.1);
|
||||
scrollbar-width: thin;
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -163,6 +165,7 @@ h2 {
|
|||
justify-content: center;
|
||||
font-size: 18px;
|
||||
border-radius: 4px;
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.flip-button-back {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
mask-image: linear-gradient(to right, transparent, black 0%, black 99%, transparent 100%);
|
||||
-webkit-mask-image: linear-gradient(to right, transparent, black 0%, black 99%, transparent 100%);
|
||||
}
|
||||
|
||||
.carousel-container {
|
||||
|
|
|
|||
|
|
@ -5,30 +5,7 @@
|
|||
@if (chapter && series && libraryType !== null) {
|
||||
<div class="row mb-0 mb-xl-3 info-container">
|
||||
<div [ngClass]="mobileSeriesImgBackground === 'true' ? 'mobile-bg' : ''" class="image-container col-5 col-sm-12 col-md-12 col-lg-5 col-xl-2 col-xxl-2 col-xxxl-2 d-none d-sm-block mb-3 position-relative">
|
||||
|
||||
@if(mobileSeriesImgBackground === 'true') {
|
||||
<app-image [styles]="{'background': 'none'}" [imageUrl]="coverImage"></app-image>
|
||||
} @else {
|
||||
<app-image [styles]="{'object-fit': 'contain', 'background': 'none', 'max-height': '400px'}" [imageUrl]="coverImage"></app-image>
|
||||
}
|
||||
<!-- TODO: For when continue on chapter/issue is hooked up -->
|
||||
@if (chapter.pagesRead < chapter.pages && chapter.pagesRead > 0) {
|
||||
<div class="progress-banner series" ngbTooltip="{{(chapter.pagesRead / chapter.pages) * 100 | number:'1.0-1'}}%">
|
||||
<ngb-progressbar type="primary" [value]="chapter.pagesRead" [max]="chapter.pages" [showValue]="true"></ngb-progressbar>
|
||||
</div>
|
||||
}
|
||||
<div class="card-overlay"></div>
|
||||
<div class="overlay-information">
|
||||
<div class="overlay-information--centered">
|
||||
<span class="card-title library mx-auto" style="width: auto;" (click)="read()">
|
||||
<!-- Card Image -->
|
||||
<div style="height: 60px; width: 60px;">
|
||||
<i class="fa-solid fa-book text-center" aria-hidden="true" style="font-size: 2rem;line-height: 60px;width: 60px"></i>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<app-cover-image [entity]="chapter" [coverImage]="imageService.getChapterCoverImage(chapter.id)" (read)="read()"></app-cover-image>
|
||||
</div>
|
||||
<div class="col-xl-10 col-lg-7 col-md-12 col-xs-12 col-sm-12">
|
||||
<h4 class="title mb-2">
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ import {ActionService} from "../_services/action.service";
|
|||
import {PublicationStatusPipe} from "../_pipes/publication-status.pipe";
|
||||
import {DefaultDatePipe} from "../_pipes/default-date.pipe";
|
||||
import {MangaFormatPipe} from "../_pipes/manga-format.pipe";
|
||||
import {CoverImageComponent} from "../_single-module/cover-image/cover-image.component";
|
||||
|
||||
enum TabID {
|
||||
Related = 'related-tab',
|
||||
|
|
@ -139,7 +140,8 @@ enum TabID {
|
|||
PublicationStatusPipe,
|
||||
DatePipe,
|
||||
DefaultDatePipe,
|
||||
MangaFormatPipe
|
||||
MangaFormatPipe,
|
||||
CoverImageComponent
|
||||
],
|
||||
templateUrl: './chapter-detail.component.html',
|
||||
styleUrl: './chapter-detail.component.scss',
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
grid-template-columns: repeat(auto-fill, 160px);
|
||||
grid-gap: 0.5rem;
|
||||
justify-content: space-around;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -174,9 +174,9 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy,
|
|||
debugLogFilter: Array<string> = ['[PREFETCH]', '[Intersection]', '[Visibility]', '[Image Load]'];
|
||||
|
||||
/**
|
||||
* Width override for maunal width control
|
||||
* Width override for manual width control
|
||||
* 2 observables needed to avoid flickering, probably due to data races, when changing the width
|
||||
* this allows to precicely define execution order
|
||||
* this allows to precisely define execution order
|
||||
*/
|
||||
widthOverride$ : Observable<string> = new Observable<string>();
|
||||
widthSliderValue$ : Observable<string> = new Observable<string>();
|
||||
|
|
@ -247,7 +247,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy,
|
|||
|
||||
this.widthOverride$ = this.widthSliderValue$;
|
||||
|
||||
//perfom jump so the page stays in view
|
||||
//perform jump so the page stays in view
|
||||
this.widthSliderValue$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(val => {
|
||||
this.currentPageElem = this.document.querySelector('img#page-' + this.pageNum);
|
||||
if(!this.currentPageElem)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,22 @@ $pointer-offset: 5px;
|
|||
overflow: auto;
|
||||
text-align: center;
|
||||
//height: calc(var(--vh)*100); // this needs to be applied on the DOM because it breaks infinite scroller
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
background-color: transparent; /*make scrollbar space invisible */
|
||||
width: inherit;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: var(--default-state-scrollbar); /*makes it invisible when not hovering*/
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: var(--primary-color-scrollbar); /*On hover, it will turn grey*/
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -4,34 +4,11 @@
|
|||
@if (series && seriesMetadata && libraryType !== null) {
|
||||
<div [ngStyle]="{'height': ScrollingBlockHeight}" class="main-container container-fluid" #scrollingBlock>
|
||||
<div class="row mb-0 mb-xl-3 info-container">
|
||||
<div [ngClass]="mobileSeriesImgBackground === 'true' ? 'mobile-bg' : ''" class="image-container col-5 col-sm-6 col-md-5 col-lg-5 col-xl-2 col-xxl-2 col-xxxl-2 d-none d-sm-block mb-3 position-relative">
|
||||
@if(mobileSeriesImgBackground === 'true') {
|
||||
<app-image [styles]="{'background': 'none'}" [imageUrl]="seriesImage"></app-image>
|
||||
} @else {
|
||||
<app-image [styles]="{'object-fit': 'contain', 'background': 'none', 'max-height': '400px'}" [imageUrl]="seriesImage"></app-image>
|
||||
}
|
||||
@if (series.pagesRead < series.pages && hasReadingProgress) {
|
||||
<div class="progress-banner series" ngbTooltip="{{(series.pagesRead / series.pages) * 100 | number:'1.0-1'}}%">
|
||||
<ngb-progressbar type="primary" [value]="series.pagesRead" [max]="series.pages" [showValue]="true"></ngb-progressbar>
|
||||
</div>
|
||||
<div class="under-image">
|
||||
{{t('continue-from', {title: ContinuePointTitle})}}
|
||||
</div>
|
||||
|
||||
}
|
||||
<div class="card-overlay"></div>
|
||||
<div class="overlay-information">
|
||||
<div class="overlay-information--centered">
|
||||
<span class="card-title library mx-auto" style="width: auto;" (click)="read()">
|
||||
<!-- Card Image -->
|
||||
<div>
|
||||
<i class="fa-solid fa-book text-center" aria-hidden="true"></i>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div [ngClass]="mobileSeriesImgBackground === 'true' ? 'mobile-bg' : ''"
|
||||
class="image-container series col-5 col-sm-6 col-md-5 col-lg-5 col-xl-2 col-xxl-2 col-xxxl-2 d-none d-sm-block mb-3 position-relative">
|
||||
<app-cover-image [entity]="series" [coverImage]="imageService.getSeriesCoverImage(series.id)" [continueTitle]="ContinuePointTitle" (read)="read()"></app-cover-image>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-10 col-lg-7 col-md-12 col-xs-12 col-sm-12">
|
||||
<h4 class="title mb-2">
|
||||
<span>{{series.name}}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
grid-template-columns: repeat(auto-fill, 160px);
|
||||
grid-gap: 0.5rem;
|
||||
justify-content: space-around;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
::ng-deep .carousel-container .header i.fa-plus, ::ng-deep .carousel-container .header i.fa-pen {
|
||||
|
|
|
|||
|
|
@ -145,6 +145,7 @@ import {CollectionTagService} from "../../../_services/collection-tag.service";
|
|||
import {UserCollection} from "../../../_models/collection-tag";
|
||||
import {SeriesFormatComponent} from "../../../shared/series-format/series-format.component";
|
||||
import {MangaFormatPipe} from "../../../_pipes/manga-format.pipe";
|
||||
import {CoverImageComponent} from "../../../_single-module/cover-image/cover-image.component";
|
||||
|
||||
|
||||
enum TabID {
|
||||
|
|
@ -179,7 +180,7 @@ interface StoryLineItem {
|
|||
NgClass, NgOptimizedImage, ProviderImagePipe, AsyncPipe, PersonBadgeComponent, DetailsTabComponent, ChapterCardComponent,
|
||||
VolumeCardComponent, JsonPipe, AgeRatingPipe, DefaultValuePipe, ExternalRatingComponent, ReadMoreComponent, ReadTimePipe,
|
||||
RouterLink, TimeAgoPipe, AgeRatingImageComponent, CompactNumberPipe, IconAndTitleComponent, SafeHtmlPipe, BadgeExpanderComponent,
|
||||
A11yClickDirective, ReadTimeLeftPipe, PublicationStatusPipe, MetadataDetailRowComponent, DownloadButtonComponent, RelatedTabComponent, SeriesFormatComponent, MangaFormatPipe]
|
||||
A11yClickDirective, ReadTimeLeftPipe, PublicationStatusPipe, MetadataDetailRowComponent, DownloadButtonComponent, RelatedTabComponent, SeriesFormatComponent, MangaFormatPipe, CoverImageComponent]
|
||||
})
|
||||
export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
||||
|
||||
|
|
@ -418,8 +419,10 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
|||
chapterLocaleKey = 'common.issue-num-shorthand';
|
||||
break;
|
||||
case LibraryType.Book:
|
||||
case LibraryType.Manga:
|
||||
case LibraryType.LightNovel:
|
||||
chapterLocaleKey = 'common.book-num-shorthand';
|
||||
break;
|
||||
case LibraryType.Manga:
|
||||
case LibraryType.Images:
|
||||
chapterLocaleKey = 'common.chapter-num-shorthand';
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
@if (accountService.hasAdminRole(user)) {
|
||||
@defer (when fragment === SettingsTabId.General; prefetch on idle) {
|
||||
@if (fragment === SettingsTabId.General) {
|
||||
<div class="col-md-6 col-sm-12">
|
||||
<div class="col-xxl-6 col-12">
|
||||
<app-manage-settings></app-manage-settings>
|
||||
</div>
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
@defer (when fragment === SettingsTabId.Email; prefetch on idle) {
|
||||
@if (fragment === SettingsTabId.Email) {
|
||||
<div class="col-md-6 col-sm-12">
|
||||
<div class="col-xxl-6 col-12">
|
||||
<app-manage-email-settings></app-manage-email-settings>
|
||||
</div>
|
||||
}
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
@defer (when fragment === SettingsTabId.Media; prefetch on idle) {
|
||||
@if (fragment === SettingsTabId.Media) {
|
||||
<div class="col-md-6 col-sm-12">
|
||||
<div class="col-xxl-6 col-12">
|
||||
<app-manage-media-settings></app-manage-media-settings>
|
||||
</div>
|
||||
}
|
||||
|
|
@ -92,7 +92,7 @@
|
|||
|
||||
@defer (when fragment === SettingsTabId.Account; prefetch on idle) {
|
||||
@if (fragment === SettingsTabId.Account) {
|
||||
<div class="col-md-6 col-sm-12">
|
||||
<div class="col-xxl-6 col-12">
|
||||
<app-change-email></app-change-email>
|
||||
<div class="setting-section-break"></div>
|
||||
<app-change-password></app-change-password>
|
||||
|
|
@ -106,7 +106,7 @@
|
|||
|
||||
@defer (when fragment === SettingsTabId.Preferences; prefetch on idle) {
|
||||
@if (fragment === SettingsTabId.Preferences) {
|
||||
<div class="col-md-6 col-sm-12">
|
||||
<div class="col-xxl-6 col-12">
|
||||
<app-manga-user-preferences></app-manga-user-preferences>
|
||||
</div>
|
||||
}
|
||||
|
|
@ -122,7 +122,7 @@
|
|||
|
||||
@defer (when fragment === SettingsTabId.Clients; prefetch on idle) {
|
||||
@if (fragment === SettingsTabId.Clients) {
|
||||
<div class="col-md-6 col-sm-12">
|
||||
<div class="col-xxl-6 col-12">
|
||||
<app-manage-opds></app-manage-opds>
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
@import '../../../../theme/variables';
|
||||
|
||||
::ng-deep .side-nav.closed {
|
||||
.side-nav-item {
|
||||
.side-nav-text {
|
||||
|
|
@ -133,7 +135,7 @@ a {
|
|||
color: var(--side-nav-text-color);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@media (max-width: $grid-breakpoints-lg) {
|
||||
.side-nav-item {
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
|
|
|
|||
|
|
@ -76,12 +76,12 @@
|
|||
}
|
||||
|
||||
@if (utilityService.activeBreakpoint$ | async; as breakpoint) {
|
||||
@if (breakpoint < Breakpoint.Tablet) {
|
||||
@if (breakpoint < Breakpoint.Desktop) {
|
||||
<div class="side-nav-overlay" (click)="toggleNavBar()" [ngClass]="{'closed' : (navService.sideNavCollapsed$ | async)}"></div>
|
||||
}
|
||||
}
|
||||
|
||||
<div class="bottom" [ngClass]="{'closed' : (navService.sideNavCollapsed$ | async),
|
||||
<div class="sidenav-bottom" [ngClass]="{'closed' : (navService.sideNavCollapsed$ | async),
|
||||
'hidden': (navService.sideNavVisibility$ | async) === false || (accountService.hasValidLicense$ | async) === true}">
|
||||
@if ((accountService.hasValidLicense$ | async) === false) {
|
||||
<app-side-nav-item [ngClass]="'donate'" icon="fa-heart"
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
</div>
|
||||
</div>
|
||||
@if (utilityService.activeBreakpoint$ | async; as breakpoint) {
|
||||
@if (breakpoint < Breakpoint.Tablet) {
|
||||
@if (breakpoint < Breakpoint.Desktop) {
|
||||
<div class="side-nav-overlay" (click)="collapse()" [ngClass]="{'closed' : (navService.sideNavCollapsed$ | async)}"></div>
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
@import '../../../theme/variables';
|
||||
// TODO: Move this to a common file so it applies both ways
|
||||
.side-nav {
|
||||
padding-bottom: 10px;
|
||||
|
|
@ -8,10 +9,46 @@
|
|||
margin: 0;
|
||||
left: 10px;
|
||||
top: 73px;
|
||||
overflow-y: hidden;
|
||||
overflow-x: hidden;
|
||||
border-radius: var(--side-nav-border-radius);
|
||||
transition: width var(--side-nav-openclose-transition), background-color var(--side-nav-bg-color-transition), border-color var(--side-nav-border-transition);
|
||||
overflow: auto;
|
||||
border: var(--side-nav-border);
|
||||
scrollbar-gutter: stable both-edges;
|
||||
scrollbar-width: thin;
|
||||
|
||||
// For firefox
|
||||
@supports (-moz-appearance:none) {
|
||||
scrollbar-color: transparent transparent;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
background-color: transparent; /*make scrollbar space invisible */
|
||||
width: inherit;
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: transparent; /*makes it invisible when not hovering*/
|
||||
}
|
||||
|
||||
&:hover {
|
||||
scrollbar-width: thin;
|
||||
overflow-y: auto;
|
||||
|
||||
// For firefox
|
||||
@supports (-moz-appearance:none) {
|
||||
scrollbar-color: rgba(255,255,255,0.3) rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
visibility: visible;
|
||||
background-color: rgba(255,255,255,0.3); /*On hover, it will turn grey*/
|
||||
}
|
||||
}
|
||||
|
||||
&.no-donate {
|
||||
height: calc((var(--vh)*100) - 82px);
|
||||
|
|
@ -36,7 +73,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@media (max-width: $grid-breakpoints-lg) {
|
||||
.side-nav {
|
||||
padding: 10px 0;
|
||||
width: 55vw;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, OnInit} from '@angular/core';
|
||||
import {translate, TranslocoDirective} from "@jsverse/transloco";
|
||||
import {translate, TranslocoDirective, TranslocoService} from "@jsverse/transloco";
|
||||
import {
|
||||
bookLayoutModes,
|
||||
bookWritingStyles,
|
||||
layoutModes,
|
||||
pageLayoutModes,
|
||||
pageSplitOptions,
|
||||
pdfScrollModes,
|
||||
pdfSpreadModes,
|
||||
|
|
@ -15,7 +14,6 @@ import {
|
|||
scalingOptions
|
||||
} from "../../_models/preferences/preferences";
|
||||
import {AccountService} from "../../_services/account.service";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {BookService} from "../../book-reader/_services/book.service";
|
||||
import {Title} from "@angular/platform-browser";
|
||||
import {Router} from "@angular/router";
|
||||
|
|
@ -37,7 +35,7 @@ import {
|
|||
NgbAccordionDirective, NgbAccordionHeader,
|
||||
NgbAccordionItem, NgbTooltip
|
||||
} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {NgForOf, NgIf, NgStyle, NgTemplateOutlet, TitleCasePipe} from "@angular/common";
|
||||
import {NgStyle, NgTemplateOutlet, TitleCasePipe} from "@angular/common";
|
||||
import {ColorPickerModule} from "ngx-color-picker";
|
||||
import {SettingTitleComponent} from "../../settings/_components/setting-title/setting-title.component";
|
||||
import {SettingItemComponent} from "../../settings/_components/setting-item/setting-item.component";
|
||||
|
|
@ -68,12 +66,10 @@ import {PdfScrollModePipe} from "../../_pipes/pdf-scroll-mode.pipe";
|
|||
NgbAccordionBody,
|
||||
NgbAccordionHeader,
|
||||
NgbAccordionButton,
|
||||
NgIf,
|
||||
NgbTooltip,
|
||||
NgTemplateOutlet,
|
||||
TitleCasePipe,
|
||||
ColorPickerModule,
|
||||
NgForOf,
|
||||
SettingTitleComponent,
|
||||
SettingItemComponent,
|
||||
PageLayoutModePipe,
|
||||
|
|
@ -113,7 +109,6 @@ export class ManageUserPreferencesComponent implements OnInit {
|
|||
protected readonly readerModes = readingModes;
|
||||
protected readonly layoutModes = layoutModes;
|
||||
protected readonly bookWritingStyles = bookWritingStyles;
|
||||
protected readonly pageLayoutModes = pageLayoutModes;
|
||||
protected readonly bookLayoutModes = bookLayoutModes;
|
||||
protected readonly pdfSpreadModes = pdfSpreadModes;
|
||||
protected readonly pdfThemes = pdfThemes;
|
||||
|
|
@ -219,7 +214,6 @@ export class ManageUserPreferencesComponent implements OnInit {
|
|||
tap(prefs => {
|
||||
if (this.user) {
|
||||
this.user.preferences = {...prefs};
|
||||
this.reset();
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -6,33 +6,8 @@
|
|||
|
||||
@if (volume && series && libraryType !== null) {
|
||||
<div class="row mb-0 mb-xl-3 info-container">
|
||||
<div [ngClass]="mobileSeriesImgBackground === 'true' ? 'mobile-bg' : ''" class="image-container col-5 col-sm-6 col-md-5 col-lg-5 col-xl-2 col-xxl-2 col-xxxl-2 d-none d-sm-block mb-3">
|
||||
|
||||
@if(mobileSeriesImgBackground === 'true') {
|
||||
<app-image [styles]="{'background': 'none'}" [imageUrl]="coverImage"></app-image>
|
||||
} @else {
|
||||
<app-image [styles]="{'object-fit': 'contain', 'background': 'none', 'max-height': '400px'}" [imageUrl]="coverImage"></app-image>
|
||||
}
|
||||
@if (volume.pagesRead < volume.pages && volume.pagesRead > 0) {
|
||||
<div class="progress-banner series" ngbTooltip="{{(volume.pagesRead / volume.pages) * 100 | number:'1.0-1'}}%">
|
||||
<ngb-progressbar type="primary" [value]="volume.pagesRead" [max]="volume.pages" [showValue]="true"></ngb-progressbar>
|
||||
</div>
|
||||
@if (currentlyReadingChapter) {
|
||||
<div class="under-image">
|
||||
{{t('continue-from', {title: ContinuePointTitle})}}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
<div class="overlay-information">
|
||||
<div class="overlay-information--centered">
|
||||
<span class="card-title library mx-auto" style="width: auto;" (click)="readVolume()">
|
||||
<!-- Card Image -->
|
||||
<div style="height: 60px; width: 60px;">
|
||||
<i class="fa-solid fa-book text-center" aria-hidden="true" style="font-size: 2rem;line-height: 60px;width: 60px"></i>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div [ngClass]="mobileSeriesImgBackground === 'true' ? 'mobile-bg' : ''" class="image-container col-5 col-sm-6 col-md-5 col-lg-5 col-xl-2 col-xxl-2 col-xxxl-2 d-none d-sm-block mb-3 position-relative">
|
||||
<app-cover-image [entity]="volume" [coverImage]="imageService.getVolumeCoverImage(volume.id)" [continueTitle]="ContinuePointTitle" (read)="readVolume()"></app-cover-image>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-10 col-lg-7 col-md-12 col-xs-12 col-sm-12">
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
grid-template-columns: repeat(auto-fill, 160px);
|
||||
grid-gap: 0.5rem;
|
||||
justify-content: space-around;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ import {EditChapterModalComponent} from "../_single-module/edit-chapter-modal/ed
|
|||
import {BulkOperationsComponent} from "../cards/bulk-operations/bulk-operations.component";
|
||||
import {DefaultDatePipe} from "../_pipes/default-date.pipe";
|
||||
import {MangaFormatPipe} from "../_pipes/manga-format.pipe";
|
||||
import {CoverImageComponent} from "../_single-module/cover-image/cover-image.component";
|
||||
|
||||
enum TabID {
|
||||
|
||||
|
|
@ -129,47 +130,48 @@ interface VolumeCast extends IHasCast {
|
|||
@Component({
|
||||
selector: 'app-volume-detail',
|
||||
standalone: true,
|
||||
imports: [
|
||||
LoadingComponent,
|
||||
NgbNavOutlet,
|
||||
DetailsTabComponent,
|
||||
NgbNavItem,
|
||||
NgbNavLink,
|
||||
NgbNavContent,
|
||||
NgbNav,
|
||||
ReadMoreComponent,
|
||||
AsyncPipe,
|
||||
NgbDropdownItem,
|
||||
NgbDropdownMenu,
|
||||
NgbDropdown,
|
||||
NgbDropdownToggle,
|
||||
ReadTimePipe,
|
||||
AgeRatingPipe,
|
||||
EntityTitleComponent,
|
||||
RouterLink,
|
||||
NgbProgressbar,
|
||||
DecimalPipe,
|
||||
NgbTooltip,
|
||||
ImageComponent,
|
||||
NgStyle,
|
||||
NgClass,
|
||||
TranslocoDirective,
|
||||
CardItemComponent,
|
||||
VirtualScrollerModule,
|
||||
ChapterCardComponent,
|
||||
DefaultValuePipe,
|
||||
RelatedTabComponent,
|
||||
AgeRatingImageComponent,
|
||||
CompactNumberPipe,
|
||||
BadgeExpanderComponent,
|
||||
MetadataDetailRowComponent,
|
||||
DownloadButtonComponent,
|
||||
CardActionablesComponent,
|
||||
BulkOperationsComponent,
|
||||
DatePipe,
|
||||
DefaultDatePipe,
|
||||
MangaFormatPipe
|
||||
],
|
||||
imports: [
|
||||
LoadingComponent,
|
||||
NgbNavOutlet,
|
||||
DetailsTabComponent,
|
||||
NgbNavItem,
|
||||
NgbNavLink,
|
||||
NgbNavContent,
|
||||
NgbNav,
|
||||
ReadMoreComponent,
|
||||
AsyncPipe,
|
||||
NgbDropdownItem,
|
||||
NgbDropdownMenu,
|
||||
NgbDropdown,
|
||||
NgbDropdownToggle,
|
||||
ReadTimePipe,
|
||||
AgeRatingPipe,
|
||||
EntityTitleComponent,
|
||||
RouterLink,
|
||||
NgbProgressbar,
|
||||
DecimalPipe,
|
||||
NgbTooltip,
|
||||
ImageComponent,
|
||||
NgStyle,
|
||||
NgClass,
|
||||
TranslocoDirective,
|
||||
CardItemComponent,
|
||||
VirtualScrollerModule,
|
||||
ChapterCardComponent,
|
||||
DefaultValuePipe,
|
||||
RelatedTabComponent,
|
||||
AgeRatingImageComponent,
|
||||
CompactNumberPipe,
|
||||
BadgeExpanderComponent,
|
||||
MetadataDetailRowComponent,
|
||||
DownloadButtonComponent,
|
||||
CardActionablesComponent,
|
||||
BulkOperationsComponent,
|
||||
DatePipe,
|
||||
DefaultDatePipe,
|
||||
MangaFormatPipe,
|
||||
CoverImageComponent
|
||||
],
|
||||
templateUrl: './volume-detail.component.html',
|
||||
styleUrl: './volume-detail.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
|
|
@ -310,8 +312,10 @@ export class VolumeDetailComponent implements OnInit {
|
|||
chapterLocaleKey = 'common.issue-num-shorthand';
|
||||
break;
|
||||
case LibraryType.Book:
|
||||
case LibraryType.Manga:
|
||||
case LibraryType.LightNovel:
|
||||
chapterLocaleKey = 'common.book-num-shorthand';
|
||||
break;
|
||||
case LibraryType.Manga:
|
||||
case LibraryType.Images:
|
||||
chapterLocaleKey = 'common.chapter-num-shorthand';
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -10,4 +10,39 @@
|
|||
overflow-y: auto;
|
||||
position: relative;
|
||||
overscroll-behavior-y: none;
|
||||
scrollbar-gutter: stable;
|
||||
scrollbar-width: thin;
|
||||
|
||||
// For firefox
|
||||
@supports (-moz-appearance:none) {
|
||||
scrollbar-color: transparent transparent;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
background-color: transparent; /*make scrollbar space invisible */
|
||||
width: inherit;
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: transparent; /*makes it invisible when not hovering*/
|
||||
}
|
||||
|
||||
&:hover {
|
||||
scrollbar-width: thin;
|
||||
overflow-y: auto;
|
||||
|
||||
// For firefox
|
||||
@supports (-moz-appearance:none) {
|
||||
scrollbar-color: rgba(255,255,255,0.3) rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
visibility: visible;
|
||||
background-color: rgba(255,255,255,0.3); /*On hover, it will turn grey*/
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue