Co-authored-by: Joseph Milazzo <joseph.v.milazzo@gmail.com>
This commit is contained in:
Robbie Davis 2024-08-26 10:52:46 -04:00 committed by GitHub
parent fc644985be
commit 62383042b0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
70 changed files with 741 additions and 399 deletions

View file

@ -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;
/**

View file

@ -0,0 +1,4 @@
export interface IHasProgress {
pages: number;
pagesRead: number;
}

View file

@ -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;
/**

View file

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

View file

@ -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);
}

View file

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

View file

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

View file

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

View file

@ -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();
}

View file

@ -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">

View file

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

View file

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

View file

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

View file

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

View file

@ -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,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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,

View file

@ -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'],

View file

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

View file

@ -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();
}
}

View file

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

View file

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

View file

@ -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();

View file

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

View file

@ -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)]));

View file

@ -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',

View file

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

View file

@ -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 {

View file

@ -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 {

View file

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

View file

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

View file

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

View file

@ -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({

View file

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

View file

@ -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>> = [];

View file

@ -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>
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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">

View file

@ -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',

View file

@ -20,6 +20,7 @@
grid-template-columns: repeat(auto-fill, 160px);
grid-gap: 0.5rem;
justify-content: space-around;
padding-bottom: 20px;
}

View file

@ -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)

View file

@ -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*/
}
}
}

View file

@ -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}}

View file

@ -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 {

View file

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

View file

@ -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>
}

View file

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

View file

@ -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"

View file

@ -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>
}
}

View file

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

View file

@ -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();
}
})

View file

@ -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">

View file

@ -17,6 +17,7 @@
grid-template-columns: repeat(auto-fill, 160px);
grid-gap: 0.5rem;
justify-content: space-around;
padding-bottom: 20px;
}

View file

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

View file

@ -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*/
}
}
}