mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-24 00:52:23 -04:00
UX Pass 6 (#3131)
Co-authored-by: Joseph Milazzo <joseph.v.milazzo@gmail.com>
This commit is contained in:
parent
fc644985be
commit
62383042b0
@ -259,7 +259,7 @@ public class ScannerServiceTests : AbstractDbTest
|
||||
comicInfo.AppendLine("<ComicInfo>");
|
||||
|
||||
// People Tags
|
||||
string[] people = { /* Your list of people here */ };
|
||||
string[] people = { "Joe Shmo", "Tommy Two Hands"};
|
||||
string[] genres = { /* Your list of genres here */ };
|
||||
|
||||
void AddRandomTag(string tagName, string[] choices)
|
||||
|
@ -164,8 +164,6 @@ public class SettingsController : BaseApiController
|
||||
|
||||
foreach (var setting in currentSettings)
|
||||
{
|
||||
UpdateSchedulingSettings(setting, updateSettingsDto);
|
||||
|
||||
if (setting.Key == ServerSettingKey.OnDeckProgressDays &&
|
||||
updateSettingsDto.OnDeckProgressDays + string.Empty != setting.Value)
|
||||
{
|
||||
@ -205,6 +203,8 @@ public class SettingsController : BaseApiController
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
}
|
||||
|
||||
UpdateSchedulingSettings(setting, updateSettingsDto);
|
||||
|
||||
UpdateEmailSettings(setting, updateSettingsDto);
|
||||
|
||||
|
||||
@ -293,14 +293,6 @@ public class SettingsController : BaseApiController
|
||||
{
|
||||
setting.Value = updateSettingsDto.AllowStatCollection + string.Empty;
|
||||
_unitOfWork.SettingsRepository.Update(setting);
|
||||
if (!updateSettingsDto.AllowStatCollection)
|
||||
{
|
||||
_taskScheduler.CancelStatsTasks();
|
||||
}
|
||||
else
|
||||
{
|
||||
await _taskScheduler.ScheduleStatsTasks();
|
||||
}
|
||||
}
|
||||
|
||||
if (setting.Key == ServerSettingKey.TotalBackups &&
|
||||
@ -341,20 +333,27 @@ public class SettingsController : BaseApiController
|
||||
{
|
||||
await _unitOfWork.CommitAsync();
|
||||
|
||||
if (!updateSettingsDto.AllowStatCollection)
|
||||
{
|
||||
_taskScheduler.CancelStatsTasks();
|
||||
}
|
||||
else
|
||||
{
|
||||
await _taskScheduler.ScheduleStatsTasks();
|
||||
}
|
||||
|
||||
if (updateBookmarks)
|
||||
{
|
||||
_directoryService.ExistOrCreate(bookmarkDirectory);
|
||||
_directoryService.CopyDirectoryToDirectory(originalBookmarkDirectory, bookmarkDirectory);
|
||||
_directoryService.ClearAndDeleteDirectory(originalBookmarkDirectory);
|
||||
BackgroundJob.Enqueue(() => UpdateBookmarkDirectory(originalBookmarkDirectory, bookmarkDirectory));
|
||||
}
|
||||
|
||||
if (updateSettingsDto.EnableFolderWatching)
|
||||
{
|
||||
await _libraryWatcher.StartWatching();
|
||||
BackgroundJob.Enqueue(() => _libraryWatcher.StartWatching());
|
||||
}
|
||||
else
|
||||
{
|
||||
_libraryWatcher.StopWatching();
|
||||
BackgroundJob.Enqueue(() => _libraryWatcher.StopWatching());
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@ -366,10 +365,18 @@ public class SettingsController : BaseApiController
|
||||
|
||||
|
||||
_logger.LogInformation("Server Settings updated");
|
||||
await _taskScheduler.ScheduleTasks();
|
||||
BackgroundJob.Enqueue(() => _taskScheduler.ScheduleTasks());
|
||||
|
||||
return Ok(updateSettingsDto);
|
||||
}
|
||||
|
||||
public void UpdateBookmarkDirectory(string originalBookmarkDirectory, string bookmarkDirectory)
|
||||
{
|
||||
_directoryService.ExistOrCreate(bookmarkDirectory);
|
||||
_directoryService.CopyDirectoryToDirectory(originalBookmarkDirectory, bookmarkDirectory);
|
||||
_directoryService.ClearAndDeleteDirectory(originalBookmarkDirectory);
|
||||
}
|
||||
|
||||
private void UpdateSchedulingSettings(ServerSetting setting, ServerSettingDto updateSettingsDto)
|
||||
{
|
||||
if (setting.Key == ServerSettingKey.TaskBackup && updateSettingsDto.TaskBackup != setting.Value)
|
||||
|
@ -175,6 +175,10 @@ public class SeriesService : ISeriesService
|
||||
series.Metadata.Genres.Add(genre);
|
||||
}, () => series.Metadata.GenresLocked = true);
|
||||
}
|
||||
else
|
||||
{
|
||||
series.Metadata.Genres = new List<Genre>();
|
||||
}
|
||||
|
||||
|
||||
if (updateSeriesMetadataDto.SeriesMetadata?.Tags is {Count: > 0})
|
||||
@ -188,6 +192,10 @@ public class SeriesService : ISeriesService
|
||||
series.Metadata.Tags.Add(tag);
|
||||
}, () => series.Metadata.TagsLocked = true);
|
||||
}
|
||||
else
|
||||
{
|
||||
series.Metadata.Tags = new List<Tag>();
|
||||
}
|
||||
|
||||
if (updateSeriesMetadataDto.SeriesMetadata != null)
|
||||
{
|
||||
|
@ -3,7 +3,7 @@
|
||||
We're always looking for people to help make Kavita even better, there are a number of ways to contribute.
|
||||
|
||||
## Documentation ##
|
||||
Setup guides, FAQ, the more information we have on the [wiki](https://wiki.kavitareader.com/) the better.
|
||||
Setup guides, FAQ, the more information we have on the [wiki](https://wiki.kavitareader.com/contributing) the better.
|
||||
|
||||
## Development ##
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
@import './theme/variables';
|
||||
|
||||
.title {
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
@ -142,7 +144,7 @@
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@media (max-width: $grid-breakpoints-lg) {
|
||||
.carousel-tabs-container {
|
||||
mask-image: linear-gradient(transparent, black 0%, black 90%, transparent 100%);
|
||||
-webkit-mask-image: linear-gradient(to right, transparent, black 0%, black 90%, transparent 100%);
|
||||
@ -155,7 +157,7 @@
|
||||
}
|
||||
|
||||
/* col-lg */
|
||||
@media screen and (max-width: 991px) {
|
||||
@media (max-width: $grid-breakpoints-lg) {
|
||||
.image-container.mobile-bg{
|
||||
width: 100vw;
|
||||
top: calc(var(--nav-offset) - 20px);
|
||||
@ -194,17 +196,9 @@
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@media (max-width: $grid-breakpoints-lg) {
|
||||
.carousel-tabs-container {
|
||||
mask-image: linear-gradient(to right, transparent, black 0%, black 90%, transparent 100%);
|
||||
-webkit-mask-image: linear-gradient(to right, transparent, black 0%, black 90%, transparent 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.under-image {
|
||||
background-color: var(--breadcrumb-bg-color);
|
||||
color: white;
|
||||
border-bottom-left-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
@ -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*/
|
||||
}
|
||||
}
|
||||
}
|
@ -9,7 +9,7 @@
|
||||
},
|
||||
|
||||
"dashboard": {
|
||||
"no-libraries": "There are no libraries setup yet. Configure some in",
|
||||
"no-libraries": "There are no libraries setup yet. Create some in",
|
||||
"server-settings-link": "Server settings",
|
||||
"not-granted": "You haven't been granted access to any libraries.",
|
||||
"on-deck-title": "On Deck",
|
||||
@ -246,7 +246,7 @@
|
||||
"platform-label": "Platform",
|
||||
"email-label": "Email",
|
||||
"name-label": "Name",
|
||||
"email-setup-alert": "Want to send files to your devices? Have the your admin setup Email settings first!",
|
||||
"email-setup-alert": "Want to send files to your devices? Have your admin setup Email settings first!",
|
||||
"add": "{{common.add}}",
|
||||
"delete": "{{common.delete}}",
|
||||
"edit": "{{common.edit}}",
|
||||
@ -288,7 +288,7 @@
|
||||
"email-not-confirmed": "This email is not confirmed",
|
||||
"email-confirmed": "This email is confirmed",
|
||||
"email-updated-title": "Email Updated",
|
||||
"email-updated-description": "You can use the following link below to confirm the email for your account. If your server is externally accessible, an email will have been sent to the email and the link can be used to confirm the email.",
|
||||
"email-updated-description": "You can use the following link below to confirm the email for your account. If email settings have been configured, an email will have been sent and the link can be used to confirm the email.",
|
||||
"setup-user-account": "Setup user's account",
|
||||
"invite-url-label": "Invite Url",
|
||||
"invite-url-tooltip": "Copy this and paste in a new tab",
|
||||
@ -404,11 +404,11 @@
|
||||
},
|
||||
|
||||
"directory-picker": {
|
||||
"title": "Choose a Directory",
|
||||
"title": "Choose a Folder",
|
||||
"close": "{{common.close}}",
|
||||
"path-label": "Path",
|
||||
"path-placeholder": "Start typing or select path",
|
||||
"instructions": "Select a folder to view breadcrumb. Don't see your directory? Try checking / first.",
|
||||
"instructions": "Select a folder to enter it. Don't see your folder? Try checking / first.",
|
||||
"type-header": "Type",
|
||||
"name-header": "Name",
|
||||
"cancel": "{{common.cancel}}",
|
||||
@ -574,7 +574,7 @@
|
||||
|
||||
"reset-password": {
|
||||
"title": "Password Reset",
|
||||
"description": "Enter the email of your account. Kavita will send you an email if a valid one on file, otherwise ask the admin for the link from logs.",
|
||||
"description": "Enter the email of your account. Kavita will send you an email if a valid one is on file, otherwise ask the admin to reset your password.",
|
||||
"email-label": "{{common.email}}",
|
||||
"required-field": "{{validation.required-field}}",
|
||||
"valid-email": "{{validation.valid-email}}",
|
||||
@ -631,7 +631,7 @@
|
||||
"email": "{{common.email}}",
|
||||
"required-field": "{{common.required-field}}",
|
||||
"setup-user-title": "User invited",
|
||||
"setup-user-description": "You can use the following link below to setup the account for your user or use the copy button. You may need to log out before using the link to register a new user. If Kavita can determine your server as accessibile externally (or Host Name is set), an email will have been sent to the user and the links can be used by them to finish setting up their account. Otherwise, use the link below or in logs to manually send to them or setup their account.",
|
||||
"setup-user-description": "You can use the following link below to setup the account for your user or use the copy button. You may need to log out before using the link to register a new user.",
|
||||
"setup-user-account": "Setup user's account",
|
||||
"setup-user-account-tooltip": "Copy this and paste in a new tab. You may need to log out.",
|
||||
"invite-url-label": "Invite Url",
|
||||
@ -672,7 +672,7 @@
|
||||
"discord-validation": "This is not a valid Discord User Id. Your user id is not your discord username.",
|
||||
"activate-delete": "{{common.delete}}",
|
||||
"activate-reset": "{{common.reset}}",
|
||||
"activate-reset-tooltip": "Untie your license with this server. Requires both License and Email.",
|
||||
"activate-reset-tooltip": "Invalidate an previous registration using your license. Requires both License and Email",
|
||||
"activate-save": "{{common.save}}",
|
||||
|
||||
"kavita+-desc-part-1": "Kavita+ is a premium subscription service which unlocks features for all users on this Kavita instance. Buy a subscription to unlock ",
|
||||
@ -918,7 +918,7 @@
|
||||
"library-name-unique": "Library name must be unique",
|
||||
"last-scanned-label": "Last Scanned:",
|
||||
"type-label": "Type",
|
||||
"type-tooltip": "Library type determines how filenames are parsed and if the UI shows Chapters (Manga) vs Issues (Comics). Book work the same way as Manga but have different naming in the UI.",
|
||||
"type-tooltip": "Library type determines how filenames are parsed and if the UI shows Chapters (Manga) vs Issues (Comics). Check the wiki for more details on the differences between the library types.",
|
||||
"kavitaplus-eligible-label": "Kavita+ Eligible",
|
||||
"kavitaplus-eligible-tooltip": "Will Kavita+ pull information or support Scrobbling",
|
||||
"folder-description": "Add folders to your library",
|
||||
@ -1055,7 +1055,7 @@
|
||||
|
||||
"related-tab": {
|
||||
"reading-lists-title": "{{reading-lists.title}}",
|
||||
"collections-title": "{{side-nav.title}}",
|
||||
"collections-title": "{{side-nav.collections}}",
|
||||
"relations-title": "{{tabs.related-tab}}"
|
||||
},
|
||||
|
||||
@ -1113,7 +1113,7 @@
|
||||
|
||||
|
||||
"manage-media-issues": {
|
||||
"description-part-1": "This table contains issues found during scan or reading of your media. This list is non-managed. You can clear it at any time and use Library (Force) Scan to perform analysis. A list of some common errors and what they mean can be found on the ",
|
||||
"description-part-1": "This table contains issues found during scan or reading of your media. You can clear it at any time and use Library (Force) Scan to perform analysis. A list of some common errors and what they mean can be found on the ",
|
||||
"description-part-2": "wiki.",
|
||||
"filter-label": "{{common.filter}}",
|
||||
"clear-alerts": "Clear Alerts",
|
||||
@ -1124,7 +1124,7 @@
|
||||
},
|
||||
|
||||
"manage-email-settings": {
|
||||
"description": "In order to use some functions of Kavita like Forgot Password and Send To Device, an email provider must be setup. Other features like Password change are less secure without Email setup.",
|
||||
"description": "In order to use some functions of Kavita like Send To Device an email provider must be setup. Other features like Forgot Password require admin intervention without Email setup.",
|
||||
"setting-description": "You must fill out both Host Name and SMTP settings to use email-based functionality within Kavita.",
|
||||
"test-warning": "You must save before using Test button.",
|
||||
"send-to-warning": "If you want Send to Device to work you must setup your email settings",
|
||||
@ -1207,7 +1207,7 @@
|
||||
|
||||
"manage-scrobble-errors": {
|
||||
"title": "Scrobbling Errors",
|
||||
"description": "This table contains issues found during scrobbling. This list is non-managed. You can clear it at any time and wait for the next scrobble upload to see. If there is an unknown series, you are best correcting the series name or localized series name or adding a weblink for the providers.",
|
||||
"description": "This table contains issues found during scrobbling. You can clear it at any time and wait for the next scrobble upload to see. If there is an unknown series, you are best correcting the series name or localized series name or adding a weblink for the providers.",
|
||||
"filter-label": "{{common.filter}}",
|
||||
"clear-errors": "Clear Errors",
|
||||
"series-header": "Series",
|
||||
@ -1644,7 +1644,7 @@
|
||||
"close": "{{common.close}}",
|
||||
"title": "CBL Import",
|
||||
"import-description": "To get started, import a .cbl file. Kavita will perform multiple checks before importing. Some steps will block moving forward due to issues with the file.",
|
||||
"validate-description": "All files have been validated to see if there are any operations to do on the list. Any lists have have failed will not move to the next step. Fix the CBL files and retry.",
|
||||
"validate-description": "All files have been validated to see if there are any operations to do on the list. Any lists that have failed will not move to the next step. Fix the CBL files and retry.",
|
||||
"validate-warning": "There are issues with the CBL that will prevent an import. Correct these issues then try again.",
|
||||
"validate-no-issue-description": "No issues found with CBL, press next.",
|
||||
"dry-run-description": "This is a dry run and shows what will happen if you press Next and perform the import. All Failures will not be imported.",
|
||||
@ -1860,7 +1860,7 @@
|
||||
"added-title": "Added",
|
||||
"last-modified-title": "Last Modified",
|
||||
"view-files": "View Files",
|
||||
"pages-title": "Pages",
|
||||
"pages-title": "{{edit-chapter-modal.pages-label}}",
|
||||
"words-title": "Words",
|
||||
"chapter-title": "Chapter",
|
||||
"volume-num": "{{common.volume-num}}",
|
||||
@ -1870,7 +1870,8 @@
|
||||
"force-refresh-tooltip": "Force refresh external metadata from Kavita+",
|
||||
"loose-leaf-volume": "Loose Leaf Chapters",
|
||||
"specials-volume": "Specials",
|
||||
"release-year-validation": "{{validation.year-validation}}"
|
||||
"release-year-validation": "{{validation.year-validation}}",
|
||||
"volumes-title": "{{tab.volumes-tab}}"
|
||||
},
|
||||
|
||||
"edit-chapter-modal": {
|
||||
@ -1942,12 +1943,12 @@
|
||||
"words-label": "{{edit-chapter-modal.length-title}}",
|
||||
"pages-count": "{{edit-chapter-modal.pages-count}}",
|
||||
"words-count": "{{edit-chapter-modal.words-count}}",
|
||||
"reading-time-label": "{{edit-chapter-modal.reading-time-title}}",
|
||||
"reading-time-label": "{{edit-chapter-modal.reading-time-label}}",
|
||||
"date-added-label": "{{edit-chapter-modal.date-added-title}}",
|
||||
"size-label": "{{edit-chapter-modal.size-title}}",
|
||||
"id-label": "{{edit-chapter-modal.id-title}}",
|
||||
"links-label": "{{series-metadata-detail.links-title}}",
|
||||
"last-read-label": "{{edit-chapter-modal.last-read-title}}",
|
||||
"size-label": "{{edit-chapter-modal.size-label}}",
|
||||
"id-label": "{{edit-chapter-modal.id-label}}",
|
||||
"links-label": "{{series-metadata-detail.links-label}}",
|
||||
"last-read-label": "{{edit-chapter-modal.last-read-label}}",
|
||||
"range-hours": "{{value}} {{hourWord}}",
|
||||
"read-time-label": "{{edit-chapter-modal.read-time-label}}",
|
||||
"files-label": "{{edit-chapter-modal.files-label}}",
|
||||
@ -2072,10 +2073,10 @@
|
||||
"unknown-crit": "There was an unknown critical error",
|
||||
"user-not-auth": "User is not authenticated",
|
||||
"error-code": "{{num}} Error",
|
||||
"download": "There was an issue downloading this file or you do not have permissions",
|
||||
"download": "There was an issue downloading this file or you do not have permission",
|
||||
"not-found": "That url does not exist",
|
||||
"generic": "Something unexpected went wrong",
|
||||
"rejected-cover-upload": "The image could not be fetched due to server refusing request. Please download and upload from file instead.",
|
||||
"rejected-cover-upload": "The image could not be fetched via the url. Please download and upload from file instead.",
|
||||
"invalid-confirmation-url": "Invalid confirmation url",
|
||||
"invalid-confirmation-email": "Invalid confirmation email",
|
||||
"invalid-password-reset-url": "Invalid reset password url",
|
||||
@ -2278,7 +2279,7 @@
|
||||
"k+-unlocked": "Kavita+ unlocked!",
|
||||
"k+-error": "There was an error when activating your license. Please try again.",
|
||||
"k+-delete-key": "This will only delete Kavita's license key and allow a buy link to show. This will not cancel your subscription! Use this only if directed by support!",
|
||||
"k+-reset-key": "This will untie your key from a server and allow you to re-register a Kavita instance.",
|
||||
"k+-reset-key": "This will invalidate a previous registration using your license and allow you to re-register a Kavita instance.",
|
||||
"k+-reset-key-success": "Your license has been un-registered. Use Edit button to re-register your instance and re-activate Kavita+",
|
||||
"library-deleted": "Library {{name}} has been removed",
|
||||
"copied-to-clipboard": "Copied to clipboard",
|
||||
@ -2309,7 +2310,7 @@
|
||||
"confirm-delete-volume": "Are you sure you want to delete this volume? It will not modify files on disk.",
|
||||
"alert-bad-theme": "There is invalid or unsafe css in the theme. Please reach out to your admin to have this corrected. Defaulting to dark theme.",
|
||||
"confirm-library-delete": "Are you sure you want to delete the {{name}} library? You cannot undo this action.",
|
||||
"confirm-library-type-change": "Changing library type will trigger a new scan with different parsing rules and may lead to series being re-created and hence you may loose progress and bookmarks. You should backup before you do this. Are you sure you want to continue?",
|
||||
"confirm-library-type-change": "Changing library type will trigger a new scan with different parsing rules and may lead to series being re-created and hence you may lose progress and bookmarks. You should backup before you do this. Are you sure you want to continue?",
|
||||
"confirm-download-size": "The {{entityType}} is {{size}}. Are you sure you want to continue?",
|
||||
"confirm-download-size-ios": "iOS has issues downloading files that are larger than 200MB, this download might not complete.",
|
||||
"list-doesnt-exist": "This list doesn't exist",
|
||||
@ -2375,7 +2376,7 @@
|
||||
"add-to-reading-list": "Add to Reading List",
|
||||
"add-to-reading-list-tooltip": "Add to a Reading List",
|
||||
"add-to-collection": "Add to Collection",
|
||||
"add-to-collection-tooltip": "A series to a collection",
|
||||
"add-to-collection-tooltip": "Add series to a collection",
|
||||
"send-to": "Send To",
|
||||
"delete": "Delete",
|
||||
"delete-tooltip": "There is no way to revert this decision",
|
||||
@ -2522,6 +2523,7 @@
|
||||
"chapter-num": "Chapter",
|
||||
"volume-num": "Volume",
|
||||
"chapter-num-shorthand": "Ch {{num}}",
|
||||
"book-num-shorthand": "Book {{num}}",
|
||||
"issue-num-shorthand": "#{{num}}",
|
||||
"volume-num-shorthand": "Vol {{num}}",
|
||||
|
||||
|
@ -103,9 +103,11 @@ app-root {
|
||||
body {
|
||||
scrollbar-gutter: stable both-edges;
|
||||
font-family: 'Poppins', sans-serif;
|
||||
overflow: hidden;
|
||||
overflow: hidden; // When this is enabled, it will break the webtoon reader. The nav.service will automatically remove/apply on toggling them
|
||||
scrollbar-color: rgba(255,255,255,0.3) rgba(0, 0, 0, 0.1);
|
||||
scrollbar-width: thin;
|
||||
|
||||
@media (max-width: 576px) {
|
||||
@media (max-width: $grid-breakpoints-lg) {
|
||||
margin-top: var(--nav-mobile-offset) !important;
|
||||
height: calc(var(--vh)* 100 - var(--nav-mobile-offset)) !important;
|
||||
}
|
||||
|
@ -12,6 +12,8 @@ $form-select-indicator-color:#cecece;
|
||||
$accordion-icon-color: #cecece;
|
||||
$accordion-icon-active-color: #cecece;
|
||||
|
||||
|
||||
|
||||
$grid-breakpoints-xs: 0;
|
||||
$grid-breakpoints-sm: 576px;
|
||||
$grid-breakpoints-md: 768px;
|
||||
|
@ -1,15 +1,26 @@
|
||||
|
||||
.bottom {
|
||||
@import '../variables';
|
||||
|
||||
.sidenav-bottom {
|
||||
position: absolute;
|
||||
bottom: 12px;
|
||||
bottom: 0;
|
||||
width: 190px;
|
||||
background: var(--side-nav-bg-color);
|
||||
border-radius: var(--side-nav-border-radius);
|
||||
font-size: 12px;
|
||||
transition: width var(--side-nav-openclose-transition);
|
||||
z-index: 999;
|
||||
left: 10px;
|
||||
|
||||
.donate {
|
||||
.side-nav-item {
|
||||
width: 100%;
|
||||
padding: 0 80px;
|
||||
|
||||
&:hover {
|
||||
background-color: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.closed {
|
||||
width: 45px;
|
||||
overflow-x: hidden;
|
||||
@ -18,7 +29,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
:host ::ng-deep .bottom .donate .side-nav-item {
|
||||
:host ::ng-deep .sidenav-bottom .donate .side-nav-item {
|
||||
justify-content: center;
|
||||
min-height: 25px;
|
||||
align-items: center;
|
||||
@ -28,35 +39,35 @@
|
||||
}
|
||||
}
|
||||
|
||||
:host ::ng-deep .bottom .donate .side-nav-item span {
|
||||
:host ::ng-deep .sidenav-bottom .donate .side-nav-item span {
|
||||
flex-grow: unset !important;
|
||||
min-width: unset !important;;
|
||||
}
|
||||
|
||||
:host ::ng-deep .bottom .donate .side-nav-item span div {
|
||||
:host ::ng-deep .sidenav-bottom .donate .side-nav-item span div {
|
||||
min-width: unset !important;
|
||||
}
|
||||
|
||||
:host ::ng-deep .bottom .donate .side-nav-item span div i{
|
||||
:host ::ng-deep .sidenav-bottom .donate .side-nav-item span div i{
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
:host ::ng-deep .bottom .donate .side-nav-item.closed span.phone-hidden div {
|
||||
:host ::ng-deep .sidenav-bottom .donate .side-nav-item.closed span.phone-hidden div {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:host ::ng-deep .bottom .donate .side-nav-item.closed span.side-nav-text div {
|
||||
:host ::ng-deep .sidenav-bottom .donate .side-nav-item.closed span.side-nav-text div {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
:host ::ng-deep .bottom .donate .side-nav-item.closed {
|
||||
@media (max-width: $grid-breakpoints-lg) {
|
||||
:host ::ng-deep .sidenav-bottom .donate .side-nav-item.closed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:host ::ng-deep .bottom .donate .side-nav-item span.phone-hidden {
|
||||
:host ::ng-deep .sidenav-bottom .donate .side-nav-item span.phone-hidden {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
@ -74,6 +85,10 @@
|
||||
transition: width var(--side-nav-openclose-transition), background-color var(--side-nav-bg-color-transition), border-color var(--side-nav-border-transition);
|
||||
border: var(--side-nav-border);
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&.preference {
|
||||
height: calc((var(--vh)*100));
|
||||
}
|
||||
@ -90,31 +105,52 @@
|
||||
&.closed {
|
||||
width: 55px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overflow-y: hidden;
|
||||
background-color: var(--side-nav-closed-bg-color);
|
||||
border: var(--side-nav-border-closed);
|
||||
}
|
||||
|
||||
.side-nav {
|
||||
overflow: auto;
|
||||
overflow-y: hidden;
|
||||
height: 100%;
|
||||
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%);
|
||||
scrollbar-gutter: stable;
|
||||
scrollbar-width: thin;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: inherit;
|
||||
// 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 {
|
||||
background-color: transparent; /*makes it invisible when not hovering*/
|
||||
visibility: visible;
|
||||
background-color: rgba(255,255,255,0.3); /*On hover, it will turn grey*/
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(255,255,255,0.3); /*On hover, it will turn grey*/
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.side-nav-item:first {
|
||||
border-top-left-radius: var(--side-nav-border-radius);
|
||||
border-top-right-radius: var(--side-nav-border-radius);
|
||||
@ -122,7 +158,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@media (max-width: $grid-breakpoints-lg) {
|
||||
.side-nav-container {
|
||||
padding: 10px 0;
|
||||
width: 55vw;
|
||||
@ -155,7 +191,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.bottom {
|
||||
.sidenav-bottom {
|
||||
display:none;
|
||||
|
||||
}
|
||||
|
@ -49,7 +49,8 @@
|
||||
--bs-body-bg: #1f2020; //#343a40;
|
||||
--body-text-color: #efefef;
|
||||
--btn-icon-filter: invert(1) grayscale(100%) brightness(200%);
|
||||
--primary-color-scrollbar: rgba(74,198,148,0.75);
|
||||
--primary-color-scrollbar: rgba(255,255,255,0.3);
|
||||
--default-state-scrollbar: transparent;
|
||||
--text-muted-color: hsla(0,0%,100%,.45);
|
||||
|
||||
/* New Color scheme */
|
||||
@ -81,7 +82,7 @@
|
||||
--theme-color: #000000;
|
||||
--color-scheme: dark;
|
||||
--tile-color: var(--primary-color);
|
||||
--nav-offset: 75px;
|
||||
--nav-offset: 70px;
|
||||
--nav-mobile-offset: 55px;
|
||||
|
||||
/* Setting Item */
|
||||
|
Loading…
x
Reference in New Issue
Block a user