Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
This commit is contained in:
Joe Milazzo 2024-08-29 08:46:17 -05:00 committed by GitHub
parent 5bf5558212
commit 79eb98a3bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
67 changed files with 1398 additions and 1006 deletions

View File

@ -50,11 +50,11 @@ jobs:
- name: Install Swashbuckle CLI
shell: bash
run: dotnet tool install -g --version 6.5.0 Swashbuckle.AspNetCore.Cli
run: dotnet tool install -g Swashbuckle.AspNetCore.Cli
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@ -68,7 +68,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
uses: github/codeql-action/autobuild@v3
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@ -81,6 +81,6 @@ jobs:
dotnet build Kavita.sln
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"

View File

@ -7,7 +7,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.8" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.0" />
<PackageReference Include="NSubstitute" Version="5.1.0" />
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="21.0.29" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="21.0.29" />

View File

@ -1141,6 +1141,41 @@ public class SeriesServiceTests : AbstractDbTest
Assert.Equal(3, series1.Relations.Single(s => s.TargetSeriesId == 3).TargetSeriesId);
}
[Fact]
public async Task UpdateRelatedSeries_ShouldAddPrequelWhenAddingSequel()
{
await ResetDb();
_context.Library.Add(new Library
{
AppUsers = new List<AppUser>
{
new AppUser
{
UserName = "majora2007"
}
},
Name = "Test LIb",
Type = LibraryType.Book,
Series = new List<Series>
{
new SeriesBuilder("Test Series").Build(),
new SeriesBuilder("Test Series Prequels").Build(),
}
});
await _context.SaveChangesAsync();
var series1 = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(1, SeriesIncludes.Related);
var series2 = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(2, SeriesIncludes.Related);
// Add relations
var addRelationDto = CreateRelationsDto(series1);
addRelationDto.Sequels.Add(2);
await _seriesService.UpdateRelatedSeries(addRelationDto);
Assert.NotNull(series1);
Assert.Equal(2, series1.Relations.Single(s => s.TargetSeriesId == 2).TargetSeriesId);
Assert.Equal(1, series2.Relations.Single(s => s.TargetSeriesId == 1).TargetSeriesId);
}
[Fact]
public async Task UpdateRelatedSeries_DeleteAllRelations()
{

View File

@ -70,7 +70,7 @@
<PackageReference Include="Hangfire.InMemory" Version="0.10.3" />
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
<PackageReference Include="Hangfire.Storage.SQLite" Version="0.4.2" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.63" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.64" />
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.14" />
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
@ -100,9 +100,9 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.7.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.7.3" />
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="8.0.2" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.0.1" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.0.2" />
<PackageReference Include="System.IO.Abstractions" Version="21.0.29" />
<PackageReference Include="System.Drawing.Common" Version="8.0.8" />
<PackageReference Include="VersOne.Epub" Version="3.3.2" />

View File

@ -22,6 +22,11 @@ public class LocaleController : BaseApiController
[HttpGet]
public ActionResult<IEnumerable<string>> GetAllLocales()
{
// Check if temp/locale_map.json exists
// If not, scan the 2 locale files and calculate empty keys or empty values
// Formulate the Locale object with Percentage
var languages = _localizationService.GetLocales().Select(c =>
{
try

View File

@ -1 +0,0 @@
{}

View File

@ -1 +0,0 @@
{}

View File

@ -625,7 +625,7 @@ public class SeriesService : ISeriesService
}
/// <summary>
/// Update the relations attached to the Series. Does not generate associated Sequel/Prequel pairs on target series.
/// Update the relations attached to the Series. Generates associated Sequel/Prequel pairs on target series.
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
@ -643,15 +643,90 @@ public class SeriesService : ISeriesService
UpdateRelationForKind(dto.AlternativeSettings, series.Relations.Where(r => r.RelationKind == RelationKind.AlternativeSetting).ToList(), series, RelationKind.AlternativeSetting);
UpdateRelationForKind(dto.AlternativeVersions, series.Relations.Where(r => r.RelationKind == RelationKind.AlternativeVersion).ToList(), series, RelationKind.AlternativeVersion);
UpdateRelationForKind(dto.Doujinshis, series.Relations.Where(r => r.RelationKind == RelationKind.Doujinshi).ToList(), series, RelationKind.Doujinshi);
UpdateRelationForKind(dto.Prequels, series.Relations.Where(r => r.RelationKind == RelationKind.Prequel).ToList(), series, RelationKind.Prequel);
UpdateRelationForKind(dto.Sequels, series.Relations.Where(r => r.RelationKind == RelationKind.Sequel).ToList(), series, RelationKind.Sequel);
UpdateRelationForKind(dto.Editions, series.Relations.Where(r => r.RelationKind == RelationKind.Edition).ToList(), series, RelationKind.Edition);
UpdateRelationForKind(dto.Annuals, series.Relations.Where(r => r.RelationKind == RelationKind.Annual).ToList(), series, RelationKind.Annual);
await UpdatePrequelSequelRelations(dto.Prequels, series, RelationKind.Prequel);
await UpdatePrequelSequelRelations(dto.Sequels, series, RelationKind.Sequel);
if (!_unitOfWork.HasChanges()) return true;
return await _unitOfWork.CommitAsync();
}
/// <summary>
/// Updates Prequel/Sequel relations and creates reciprocal relations on target series.
/// </summary>
/// <param name="targetSeriesIds">List of target series IDs</param>
/// <param name="series">The current series being updated</param>
/// <param name="kind">The relation kind (Prequel or Sequel)</param>
private async Task UpdatePrequelSequelRelations(ICollection<int> targetSeriesIds, Series series, RelationKind kind)
{
var existingRelations = series.Relations.Where(r => r.RelationKind == kind).ToList();
// Remove relations that are not in the new list
foreach (var relation in existingRelations.Where(relation => !targetSeriesIds.Contains(relation.TargetSeriesId)))
{
series.Relations.Remove(relation);
await RemoveReciprocalRelation(series.Id, relation.TargetSeriesId, GetOppositeRelationKind(kind));
}
// Add new relations
foreach (var targetSeriesId in targetSeriesIds)
{
if (series.Relations.Any(r => r.RelationKind == kind && r.TargetSeriesId == targetSeriesId))
continue;
series.Relations.Add(new SeriesRelation
{
Series = series,
SeriesId = series.Id,
TargetSeriesId = targetSeriesId,
RelationKind = kind
});
await AddReciprocalRelation(series.Id, targetSeriesId, GetOppositeRelationKind(kind));
}
_unitOfWork.SeriesRepository.Update(series);
}
private static RelationKind GetOppositeRelationKind(RelationKind kind)
{
return kind == RelationKind.Prequel ? RelationKind.Sequel : RelationKind.Prequel;
}
private async Task AddReciprocalRelation(int sourceSeriesId, int targetSeriesId, RelationKind kind)
{
var targetSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(targetSeriesId, SeriesIncludes.Related);
if (targetSeries == null) return;
if (targetSeries.Relations.Any(r => r.RelationKind == kind && r.TargetSeriesId == sourceSeriesId))
return;
targetSeries.Relations.Add(new SeriesRelation
{
Series = targetSeries,
SeriesId = targetSeriesId,
TargetSeriesId = sourceSeriesId,
RelationKind = kind
});
_unitOfWork.SeriesRepository.Update(targetSeries);
}
private async Task RemoveReciprocalRelation(int sourceSeriesId, int targetSeriesId, RelationKind kind)
{
var targetSeries = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(targetSeriesId, SeriesIncludes.Related);
if (targetSeries == null) return;
var relationToRemove = targetSeries.Relations.FirstOrDefault(r => r.RelationKind == kind && r.TargetSeriesId == sourceSeriesId);
if (relationToRemove != null)
{
targetSeries.Relations.Remove(relationToRemove);
_unitOfWork.SeriesRepository.Update(targetSeries);
}
}
/// <summary>
/// Applies the provided list to the series. Adds new relations and removes deleted relations.

View File

@ -1,5 +1,3 @@
$image-height: 232.91px;
$image-width: 160px;
@ -21,7 +19,6 @@ $image-width: 160px;
outline: 2px solid var(--primary-color);
}
.progress-banner {
width: $image-width;
height: 5px;
@ -63,7 +60,6 @@ $image-width: 160px;
transform: rotate(45deg);
}
.bulk-mode {
position: absolute;
top: 5px;
@ -90,7 +86,6 @@ $image-width: 160px;
border-width: 0;
}
.overlay {
&:hover {
.bulk-mode {
@ -102,18 +97,18 @@ $image-width: 160px;
visibility: visible;
.overlay-information {
visibility: visible;
display: block;
visibility: visible;
display: block;
}
& + .meta-title {
display: -webkit-box;
visibility: visible;
pointer-events: none;
display: -webkit-box;
visibility: visible;
pointer-events: none;
}
}
}
.overlay-information {
.overlay-information {
position: absolute;
top: 0;
left: 0;
@ -124,24 +119,24 @@ $image-width: 160px;
border-top-right-radius: 4px;
&:hover {
background-color: var(--card-overlay-hover-bg-color);
cursor: pointer;
background-color: var(--card-overlay-hover-bg-color);
cursor: pointer;
}
.overlay-information--centered {
position: absolute;
border-radius: 15px;
background-color: rgba(0, 0, 0, .7);
border-radius: 50px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 115;
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;
}
&:hover {
background-color: var(--primary-color) !important;
cursor: pointer;
}
}
}
}

View File

@ -20,6 +20,11 @@
.main-container {
overflow: unset !important;
margin-top: 15px;
}
::ng-deep .badge-expander .content a {
font-size: 0.8rem;
}
.btn-group > .btn.dropdown-toggle-split:not(first-child){

View File

@ -245,46 +245,58 @@ export class ColorscapeService {
const secondaryHSL = this.rgbToHsl(secondary);
if (isDarkTheme) {
const lighterHSL = this.adjustHue(secondaryHSL, 30);
lighterHSL.s = Math.min(lighterHSL.s + 0.2, 1);
lighterHSL.l = Math.min(lighterHSL.l + 0.1, 0.6);
const darkerHSL = { ...primaryHSL };
darkerHSL.l = Math.max(darkerHSL.l - 0.3, 0.1);
const complementaryHSL = this.adjustHue(primaryHSL, 180);
complementaryHSL.s = Math.min(complementaryHSL.s + 0.1, 1);
complementaryHSL.l = Math.max(complementaryHSL.l - 0.2, 0.2);
return {
primary: this.rgbToHex(primary),
lighter: this.rgbToHex(this.hslToRgb(lighterHSL)),
darker: this.rgbToHex(this.hslToRgb(darkerHSL)),
complementary: this.rgbToHex(this.hslToRgb(complementaryHSL))
};
return this.calculateDarkThemeColors(secondaryHSL, primaryHSL, primary);
} else {
// NOTE: Light themes look bad in general with this system.
const lighterHSL = { ...primaryHSL };
lighterHSL.s = Math.max(lighterHSL.s - 0.3, 0);
lighterHSL.l = Math.min(lighterHSL.l + 0.5, 0.95);
const darkerHSL = { ...primaryHSL };
darkerHSL.s = Math.max(darkerHSL.s - 0.1, 0);
darkerHSL.l = Math.min(darkerHSL.l + 0.3, 0.9);
const complementaryHSL = this.adjustHue(primaryHSL, 180);
complementaryHSL.s = Math.max(complementaryHSL.s - 0.2, 0);
complementaryHSL.l = Math.min(complementaryHSL.l + 0.4, 0.9);
return {
primary: this.rgbToHex(primary),
lighter: this.rgbToHex(this.hslToRgb(lighterHSL)),
darker: this.rgbToHex(this.hslToRgb(darkerHSL)),
complementary: this.rgbToHex(this.hslToRgb(complementaryHSL))
};
return this.calculateLightThemeDarkColors(primaryHSL, primary);
}
}
private calculateLightThemeDarkColors(primaryHSL: { h: number; s: number; l: number }, primary: RGB) {
const lighterHSL = {...primaryHSL};
lighterHSL.s = Math.max(lighterHSL.s - 0.3, 0);
lighterHSL.l = Math.min(lighterHSL.l + 0.5, 0.95);
const darkerHSL = {...primaryHSL};
darkerHSL.s = Math.max(darkerHSL.s - 0.1, 0);
darkerHSL.l = Math.min(darkerHSL.l + 0.3, 0.9);
const complementaryHSL = this.adjustHue(primaryHSL, 180);
complementaryHSL.s = Math.max(complementaryHSL.s - 0.2, 0);
complementaryHSL.l = Math.min(complementaryHSL.l + 0.4, 0.9);
return {
primary: this.rgbToHex(primary),
lighter: this.rgbToHex(this.hslToRgb(lighterHSL)),
darker: this.rgbToHex(this.hslToRgb(darkerHSL)),
complementary: this.rgbToHex(this.hslToRgb(complementaryHSL))
};
}
private calculateDarkThemeColors(secondaryHSL: { h: number; s: number; l: number }, primaryHSL: {
h: number;
s: number;
l: number
}, primary: RGB) {
const lighterHSL = this.adjustHue(secondaryHSL, 30);
lighterHSL.s = Math.min(lighterHSL.s + 0.2, 1);
lighterHSL.l = Math.min(lighterHSL.l + 0.1, 0.6);
const darkerHSL = {...primaryHSL};
darkerHSL.l = Math.max(darkerHSL.l - 0.3, 0.1);
const complementaryHSL = this.adjustHue(primaryHSL, 180);
complementaryHSL.s = Math.min(complementaryHSL.s + 0.1, 1);
complementaryHSL.l = Math.max(complementaryHSL.l - 0.2, 0.2);
return {
primary: this.rgbToHex(primary),
lighter: this.rgbToHex(this.hslToRgb(lighterHSL)),
darker: this.rgbToHex(this.hslToRgb(darkerHSL)),
complementary: this.rgbToHex(this.hslToRgb(complementaryHSL))
};
}
private hexToRgb(hex: string): RGB {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {

View File

@ -119,6 +119,13 @@
text-align: center;
position: absolute;
width: 100%;
font-size: 0.8rem;
-webkit-line-clamp: 1;
font-size: 0.8rem;
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
padding: 0 10px 0 0;
}
}

View File

@ -13,7 +13,7 @@
<ng-template #carouselItem let-item>
<app-card-item [title]="item.title" [entity]="item"
[suppressLibraryLink]="true" [imageUrl]="imageService.getCollectionCoverImage(item.id)"
(clicked)="openCollection(item)"></app-card-item>
(clicked)="openCollection(item)" [linkUrl]="'/collections/' + item.id"></app-card-item>
</ng-template>
</app-carousel-reel>
}
@ -24,7 +24,7 @@
<ng-template #carouselItem let-item>
<app-card-item [title]="item.title" [entity]="item"
[suppressLibraryLink]="true" [imageUrl]="imageService.getReadingListCoverImage(item.id)"
(clicked)="openReadingList(item)"></app-card-item>
(clicked)="openReadingList(item)" [linkUrl]="'/lists/' + item.id"></app-card-item>
</ng-template>
</app-carousel-reel>
}

View File

@ -25,7 +25,7 @@
{{library.type | libraryType}}
</td>
<td>
{{t('folder-count', {num: library.folders.length})}}
{{library.folders.length}}
</td>
<td>
{{library.lastScanned | timeAgo | defaultDate}}

View File

@ -1,3 +1,5 @@
@import "../../../theme/variables";
.custom-position {
right: 15px;
top: -42px;
@ -11,3 +13,23 @@
.list-group-item:nth-child(even) {
background-color: var(--elevation-layer1);
}
.table {
@media (max-width: $grid-breakpoints-sm) {
overflow-x: auto;
width: 100% !important;
display: block;
}
.btn-container {
@media (max-width: $grid-breakpoints-lg) {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
justify-content: center;
}
.btn {
width: 32px;
}
}
}

View File

@ -0,0 +1,21 @@
@import "../../../theme/variables";
.table {
@media (max-width: $grid-breakpoints-sm) {
overflow-x: auto;
width: 100% !important;
display: block;
}
.btn-container {
@media (max-width: $grid-breakpoints-lg) {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
justify-content: center;
}
.btn {
width: 32px;
}
}
}

View File

@ -1,4 +1,11 @@
<ng-container *transloco="let t; read: 'manage-media-settings'">
<div class="position-relative">
<button class="btn btn-secondary-outline position-absolute custom-position" (click)="resetToDefaults()" [title]="t('reset-to-default')">
<span class="phone-hidden ms-1">{{t('reset-to-default')}}</span>
</button>
</div>
<form [formGroup]="settingsForm">
<div class="mb-4">
<p>
@ -64,10 +71,6 @@
</ng-container>
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end">
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetToDefaults()">{{t('reset-to-default')}}</button>
</div>
</form>
</ng-container>

View File

@ -0,0 +1,4 @@
.custom-position {
right: 5px;
top: -42px;
}

View File

@ -67,26 +67,28 @@
}
</td>
<td>
@if (canEditMember(member)) {
<button class="btn btn-danger btn-sm me-2" (click)="deleteUser(member)"
placement="top" [ngbTooltip]="t('delete-user-tooltip')" [attr.aria-label]="t('delete-user-alt', {user: member.username | titlecase})">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
<button class="btn btn-primary btn-sm me-2" (click)="openEditUser(member)"
placement="top" [ngbTooltip]="t('edit-user-tooltip')" [attr.aria-label]="t('edit-user-alt', {user: member.username | titlecase})">
<i class="fa fa-pen" aria-hidden="true"></i>
</button>
<div class="btn-container">
@if (canEditMember(member)) {
<button class="btn btn-danger btn-sm me-2 mb-2" (click)="deleteUser(member)"
placement="top" [ngbTooltip]="t('delete-user-tooltip')" [attr.aria-label]="t('delete-user-alt', {user: member.username | titlecase})">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
<button class="btn btn-primary btn-sm me-2 mb-2" (click)="openEditUser(member)"
placement="top" [ngbTooltip]="t('edit-user-tooltip')" [attr.aria-label]="t('edit-user-alt', {user: member.username | titlecase})">
<i class="fa fa-pen" aria-hidden="true"></i>
</button>
@if (member.isPending) {
<button class="btn btn-secondary btn-sm me-2" (click)="resendEmail(member)"
placement="top" [ngbTooltip]="t('resend-invite-tooltip')" [attr.aria-label]="t('resend-invite-alt', {user: member.username | titlecase})"><i class="fa-solid fa-share-from-square" aria-hidden="true"></i></button>
<button class="btn btn-secondary btn-sm" (click)="setup(member)"
placement="top" [ngbTooltip]="t('setup-user-tooltip')" [attr.aria-label]="t('setup-user-alt', {user: member.username | titlecase})"><i class="fa-solid fa-sliders" aria-hidden="true"></i></button>
} @else {
<button class="btn btn-secondary btn-sm" (click)="updatePassword(member)"
placement="top" [ngbTooltip]="t('change-password-tooltip')" [attr.aria-label]="t('change-password-alt', {user: member.username | titlecase})"><i class="fa fa-key" aria-hidden="true"></i></button>
@if (member.isPending) {
<button class="btn btn-secondary btn-sm me-2 mb-2" (click)="resendEmail(member)"
placement="top" [ngbTooltip]="t('resend-invite-tooltip')" [attr.aria-label]="t('resend-invite-alt', {user: member.username | titlecase})"><i class="fa-solid fa-share-from-square" aria-hidden="true"></i></button>
<button class="btn btn-secondary btn-sm me-2 mb-2" (click)="setup(member)"
placement="top" [ngbTooltip]="t('setup-user-tooltip')" [attr.aria-label]="t('setup-user-alt', {user: member.username | titlecase})"><i class="fa-solid fa-sliders" aria-hidden="true"></i></button>
} @else {
<button class="btn btn-secondary btn-sm me-2 mb-2" (click)="updatePassword(member)"
placement="top" [ngbTooltip]="t('change-password-tooltip')" [attr.aria-label]="t('change-password-alt', {user: member.username | titlecase})"><i class="fa fa-key" aria-hidden="true"></i></button>
}
}
}
</div>
</td>
</tr>
}

View File

@ -1,3 +1,5 @@
@import '../../../theme/variables';
.presence {
font-size: 12px;
color: var(--primary-color);
@ -32,3 +34,24 @@
.list-group-item:nth-child(even) {
background-color: var(--elevation-layer1);
}
.table {
@media (max-width: $grid-breakpoints-lg) {
overflow-x: auto;
width: 100% !important;
display: block;
}
.btn-container {
@media (max-width: $grid-breakpoints-lg) {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
justify-content: center;
}
.btn {
width: 32px;
}
}
}

View File

@ -1,25 +1,27 @@
<ng-container *transloco="let t; read: 'all-series'">
<app-side-nav-companion-bar [hasFilter]="true" (filterOpen)="filterOpen.emit($event)" [filterActive]="filterActive">
<h4 title>
{{title}}
</h4>
<h5 subtitle *ngIf="pagination">{{t('series-count', {num: pagination.totalItems | number})}}</h5>
</app-side-nav-companion-bar>
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
<app-card-detail-layout *ngIf="filter"
[isLoading]="loadingSeries"
[items]="series"
[trackByIdentity]="trackByIdentity"
[filterSettings]="filterSettings"
[filterOpen]="filterOpen"
[jumpBarKeys]="jumpbarKeys"
(applyFilter)="updateFilter($event)"
>
<ng-template #cardItem let-item let-position="idx">
<app-series-card [series]="item" [libraryId]="item.libraryId" (reload)="loadPage()"
(selection)="bulkSelectionService.handleCardSelection('series', position, series.length, $event)"
[selected]="bulkSelectionService.isCardSelected('series', position)" [allowSelection]="true"></app-series-card>
</ng-template>
</app-card-detail-layout>
<div class="main-container container-fluid">
<ng-container *transloco="let t; read: 'all-series'">
<app-side-nav-companion-bar [hasFilter]="true" (filterOpen)="filterOpen.emit($event)" [filterActive]="filterActive">
<h4 title>
{{title}}
</h4>
<h5 subtitle *ngIf="pagination">{{t('series-count', {num: pagination.totalItems | number})}}</h5>
</app-side-nav-companion-bar>
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
<app-card-detail-layout *ngIf="filter"
[isLoading]="loadingSeries"
[items]="series"
[trackByIdentity]="trackByIdentity"
[filterSettings]="filterSettings"
[filterOpen]="filterOpen"
[jumpBarKeys]="jumpbarKeys"
(applyFilter)="updateFilter($event)"
>
<ng-template #cardItem let-item let-position="idx">
<app-series-card [series]="item" [libraryId]="item.libraryId" (reload)="loadPage()"
(selection)="bulkSelectionService.handleCardSelection('series', position, series.length, $event)"
[selected]="bulkSelectionService.isCardSelected('series', position)" [allowSelection]="true"></app-series-card>
</ng-template>
</app-card-detail-layout>
</ng-container>
</ng-container>
</div>

View File

@ -1,9 +1,11 @@
<ng-container *transloco="let t; read: 'announcements'">
<app-side-nav-companion-bar>
<h2 title>
{{t('title')}}
</h2>
</app-side-nav-companion-bar>
<div class="main-container container-fluid">
<ng-container *transloco="let t; read: 'announcements'">
<app-side-nav-companion-bar>
<h2 title>
{{t('title')}}
</h2>
</app-side-nav-companion-bar>
<app-changelog></app-changelog>
</ng-container>
<app-changelog></app-changelog>
</ng-container>
</div>

View File

@ -1,42 +1,44 @@
<ng-container *transloco="let t; read: 'changelog'">
<div class="changelog">
<p class="pb-2">
{{t('description', {installed: ''})}}
<span class="badge bg-secondary">{{t('installed')}}</span>
{{t('description-continued', {installed: ''})}}
</p>
<div class="main-container container-fluid">
<ng-container *transloco="let t; read: 'changelog'">
<div class="changelog">
<p class="pb-2">
{{t('description', {installed: ''})}}
<span class="badge bg-secondary">{{t('installed')}}</span>
{{t('description-continued', {installed: ''})}}
</p>
@for(update of updates; track update; let indx = $index) {
<div class="card w-100 mb-2" style="width: 18rem;">
<div class="card-body">
<h4 class="card-title">{{update.updateTitle}}&nbsp;
@if (update.isOnNightlyInRelease) {
<span class="badge bg-secondary">{{t('nightly', {version: update.currentVersion})}}</span>
} @else if (update.isReleaseEqual) {
<span class="badge bg-secondary">{{t('installed')}}</span>
} @else if (update.isReleaseNewer && indx === 0) {
<span class="badge bg-secondary">{{t('available')}}</span>
@for(update of updates; track update; let indx = $index) {
<div class="card w-100 mb-2" style="width: 18rem;">
<div class="card-body">
<h4 class="card-title">{{update.updateTitle}}&nbsp;
@if (update.isOnNightlyInRelease) {
<span class="badge bg-secondary">{{t('nightly', {version: update.currentVersion})}}</span>
} @else if (update.isReleaseEqual) {
<span class="badge bg-secondary">{{t('installed')}}</span>
} @else if (update.isReleaseNewer && indx === 0) {
<span class="badge bg-secondary">{{t('available')}}</span>
}
</h4>
<h6 class="card-subtitle mb-1 mt-1 text-muted">{{t('published-label')}}{{update.publishDate | date: 'short'}}</h6>
<pre class="card-text update-body">
<app-read-more [text]="update.updateBody" [maxLength]="500"></app-read-more>
</pre>
@if (!update.isDocker && (accountService.isAdmin$ | async)) {
@if (update.updateVersion === update.currentVersion) {
<a href="{{update.updateUrl}}" class="btn disabled btn-{{indx === 0 ? 'primary' : 'secondary'}} float-end" target="_blank" rel="noopener noreferrer">{{t('installed')}}</a>
} @else {
<a href="{{update.updateUrl}}" class="btn btn-{{indx === 0 ? 'primary' : 'secondary'}} float-end" target="_blank" rel="noopener noreferrer">{{t('download')}}</a>
}
}
</h4>
<h6 class="card-subtitle mb-1 mt-1 text-muted">{{t('published-label')}}{{update.publishDate | date: 'short'}}</h6>
<pre class="card-text update-body">
<app-read-more [text]="update.updateBody" [maxLength]="500"></app-read-more>
</pre>
@if (!update.isDocker && (accountService.isAdmin$ | async)) {
@if (update.updateVersion === update.currentVersion) {
<a href="{{update.updateUrl}}" class="btn disabled btn-{{indx === 0 ? 'primary' : 'secondary'}} float-end" target="_blank" rel="noopener noreferrer">{{t('installed')}}</a>
} @else {
<a href="{{update.updateUrl}}" class="btn btn-{{indx === 0 ? 'primary' : 'secondary'}} float-end" target="_blank" rel="noopener noreferrer">{{t('download')}}</a>
}
}
</div>
</div>
</div>
}
</div>
}
</div>
<app-loading [loading]="isLoading"></app-loading>
<app-loading [loading]="isLoading"></app-loading>
</ng-container>
</ng-container>
</div>

View File

@ -20,7 +20,7 @@
}
}
<div class="container-fluid" [ngClass]="{'g-0': (navService.sideNavVisibility$ | async) === false}">
<div class="" [ngClass]="{'g-0': (navService.sideNavVisibility$ | async) === false}">
<a id="content"></a>
@if (navService.sideNavVisibility$ | async) {
<div>

View File

@ -4,15 +4,13 @@
height: calc(var(--vh)* 100 - var(--nav-offset));
}
.companion-bar {
transition: all var(--side-nav-companion-bar-transistion);
margin-left: 40px;
margin-left: 60px;
overflow-y: hidden;
overflow-x: hidden;
height: calc(var(--vh)* 100 - var(--nav-mobile-offset));
padding-right: 10px;
scrollbar-gutter: stable both-edges;
scrollbar-gutter: stable;
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%);
@ -58,7 +56,6 @@
.companion-bar-content {
margin-left: 190px;
width: calc(100% - 180px);
}
@media (max-width: $grid-breakpoints-lg) {
@ -73,7 +70,7 @@
.content-wrapper {
overflow: hidden;
height: calc(var(--vh)* 100);
padding: 0 10px 0;
padding: 0;
&.closed {
overflow: auto;
@ -90,6 +87,7 @@
scrollbar-color: rgba(255,255,255,0.3) rgba(0, 0, 0, 0.1);
scrollbar-width: thin;
margin-bottom: 20px;
overflow-y: auto;
}
.companion-bar-content {

View File

@ -1,31 +1,33 @@
<ng-container *transloco="let t; read: 'bookmarks'">
<app-side-nav-companion-bar [hasFilter]="true" (filterOpen)="filterOpen.emit($event)" [filterActive]="filterActive">
<h4 title>
{{t('title')}}
</h4>
<h5 subtitle>{{t('series-count', {num: series.length | number})}}</h5>
</app-side-nav-companion-bar>
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
<app-card-detail-layout *ngIf="filter"
[isLoading]="loadingBookmarks"
[items]="series"
[filterSettings]="filterSettings"
[trackByIdentity]="trackByIdentity"
[refresh]="refresh"
[jumpBarKeys]="jumpbarKeys"
(applyFilter)="updateFilter($event)"
>
<ng-template #cardItem let-item let-position="idx">
<app-card-item [entity]="item" [title]="item.name" [imageUrl]="imageService.getSeriesCoverImage(item.id)"
[suppressArchiveWarning]="true" (clicked)="viewBookmarks(item)" [count]="seriesIds[item.id]" [allowSelection]="true"
[actions]="actions"
[selected]="bulkSelectionService.isCardSelected('bookmark', position)"
(selection)="bulkSelectionService.handleCardSelection('bookmark', position, series.length, $event)"
></app-card-item>
</ng-template>
<div class="main-container container-fluid">
<ng-container *transloco="let t; read: 'bookmarks'">
<app-side-nav-companion-bar [hasFilter]="true" (filterOpen)="filterOpen.emit($event)" [filterActive]="filterActive">
<h4 title>
{{t('title')}}
</h4>
<h5 subtitle>{{t('series-count', {num: series.length | number})}}</h5>
</app-side-nav-companion-bar>
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
<app-card-detail-layout *ngIf="filter"
[isLoading]="loadingBookmarks"
[items]="series"
[filterSettings]="filterSettings"
[trackByIdentity]="trackByIdentity"
[refresh]="refresh"
[jumpBarKeys]="jumpbarKeys"
(applyFilter)="updateFilter($event)"
>
<ng-template #cardItem let-item let-position="idx">
<app-card-item [entity]="item" [title]="item.name" [imageUrl]="imageService.getSeriesCoverImage(item.id)"
[suppressArchiveWarning]="true" (clicked)="viewBookmarks(item)" [count]="seriesIds[item.id]" [allowSelection]="true"
[actions]="actions"
[selected]="bulkSelectionService.isCardSelected('bookmark', position)"
(selection)="bulkSelectionService.handleCardSelection('bookmark', position, series.length, $event)"
></app-card-item>
</ng-template>
<ng-template #noData>
{{t('no-data')}} <a [href]="WikiLink.Bookmarks" rel="noopener noreferrer" target="_blank">{{t('no-data-2')}}<i class="fa fa-external-link-alt ms-1" aria-hidden="true"></i></a>
</ng-template>
</app-card-detail-layout>
</ng-container>
<ng-template #noData>
{{t('no-data')}} <a [href]="WikiLink.Bookmarks" rel="noopener noreferrer" target="_blank">{{t('no-data-2')}}<i class="fa fa-external-link-alt ms-1" aria-hidden="true"></i></a>
</ng-template>
</app-card-detail-layout>
</ng-container>
</div>

View File

@ -1,33 +1,35 @@
<ng-container *transloco="let t; read: 'bulk-operations'">
@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">
<div class="bulk-select-container">
<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 class="highlight">
<i class="fa fa-check me-1" aria-hidden="true"></i>
{{t('items-selected',{num: selectionCount | number})}}
</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">
<i class="fa-solid fa-circle-check" aria-hidden="true"></i>
<span class="visually-hidden">{{t('mark-as-read')}}</span>
</button>
<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>
}
<app-card-actionables [actions]="actions" labelBy="bulk-actions-header" iconClass="fa-ellipsis-h" (actionHandler)="performAction($event)"></app-card-actionables>
</span>
@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>
</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>
}

View File

@ -1,13 +1,17 @@
.bulk-select {
background-color: var(--bulk-background-color);
border-bottom: 2px solid var(--primary-color);
color: var(--bulk-selection-text-color) !important;
.bulk-select-container {
position: absolute;
.btn-icon {
color: var(--bulk-selection-text-color);
.bulk-select {
background-color: var(--bulk-selection-bg-color);
border-bottom: 2px solid var(--primary-color);
color: var(--bulk-selection-text-color) !important;
.btn-icon {
color: var(--bulk-selection-text-color);
}
}
}
.highlight {
color: var(--bulk-selection-highlight-text-color) !important;
}
}

View File

@ -48,12 +48,12 @@
flex-shrink: 0;
font-size: 13px;
overflow: hidden;
padding: 0 10px;
padding: 0 5px;
align-items: center;
justify-content: space-around;
@media (max-width: 576px) {
padding: 0 10px 0 5px;
padding: 0 5px 0 5px;
}
.btn {
@ -133,7 +133,7 @@ h2 {
cursor: pointer;
padding: 0;
margin: 0;
width: 40px;
width: 25px;
height: 25px;
position: relative;
overflow: hidden;

View File

@ -3,7 +3,7 @@ import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ContentChild,
ContentChild, DestroyRef,
ElementRef,
EventEmitter,
HostListener,
@ -17,7 +17,7 @@ import {
TrackByFunction,
ViewChild
} from '@angular/core';
import {Router} from '@angular/router';
import {NavigationEnd, NavigationStart, Router} from '@angular/router';
import {VirtualScrollerComponent, VirtualScrollerModule} from '@iharbeck/ngx-virtual-scroller';
import {FilterSettings} from 'src/app/metadata-filter/filter-settings';
import {FilterUtilitiesService} from 'src/app/shared/_services/filter-utilities.service';
@ -36,6 +36,9 @@ import {MetadataFilterComponent} from "../../metadata-filter/metadata-filter.com
import {TranslocoDirective} from "@jsverse/transloco";
import {CardActionablesComponent} from "../../_single-module/card-actionables/card-actionables.component";
import {SeriesFilterV2} from "../../_models/metadata/v2/series-filter-v2";
import {filter, map} from "rxjs/operators";
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
import {tap} from "rxjs";
const ANIMATION_TIME_MS = 0;
@ -56,6 +59,7 @@ export class CardDetailLayoutComponent implements OnInit, OnChanges {
private readonly cdRef = inject(ChangeDetectorRef);
private readonly jumpbarService = inject(JumpbarService);
private readonly router = inject(Router);
private readonly destroyRef = inject(DestroyRef);
protected readonly Breakpoint = Breakpoint;
@ -138,6 +142,14 @@ export class CardDetailLayoutComponent implements OnInit, OnChanges {
this.virtualScroller.refresh();
});
}
this.router.events.pipe(
filter(event => event instanceof NavigationStart),
takeUntilDestroyed(this.destroyRef),
map(evt => evt as NavigationStart),
tap(_ => this.tryToSaveJumpKey()),
).subscribe();
}

View File

@ -77,7 +77,11 @@
<span class="card-title" placement="top" id="{{title}}_{{entity.id}}" [ngbTooltip]="tooltipTitle" (click)="handleClick($event)" tabindex="0">
<app-promoted-icon [promoted]="isPromoted()"></app-promoted-icon>
<app-series-format [format]="format"></app-series-format>
{{title}}
@if (linkUrl) {
<a class="dark-exempt btn-icon" href="javascript:void(0);" [routerLink]="linkUrl">{{title}}</a>
} @else {
{{title}}
}
</span>
@if (actions && actions.length > 0) {
<span class="card-actions float-end">

View File

@ -145,6 +145,10 @@ export class CardItemComponent implements OnInit {
* Will generate a button to instantly read
*/
@Input() hasReadButton = false;
/**
* A method that if defined will return the url
*/
@Input() linkUrl?: string;
/**
* Event emitted when item is clicked
*/

View File

@ -1,36 +1,39 @@
<ng-container *transloco="let t; read: 'all-collections'">
<app-side-nav-companion-bar [hasFilter]="false" (filterOpen)="filterOpen.emit($event)">
<h4 title>{{t('title')}}</h4>
<h5 subtitle>{{t('item-count', {num: collections.length | number})}}</h5>
</app-side-nav-companion-bar>
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
<div class="main-container container-fluid">
<ng-container *transloco="let t; read: 'all-collections'">
<app-side-nav-companion-bar [hasFilter]="false" (filterOpen)="filterOpen.emit($event)">
<h4 title>{{t('title')}}</h4>
<h5 subtitle>{{t('item-count', {num: collections.length | number})}}</h5>
</app-side-nav-companion-bar>
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
<app-card-detail-layout
[isLoading]="isLoading"
[items]="collections"
[filterOpen]="filterOpen"
[jumpBarKeys]="jumpbarKeys"
[trackByIdentity]="trackByIdentity"
>
<ng-template #cardItem let-item let-position="idx">
<app-card-item [title]="item.title" [entity]="item" [actions]="collectionTagActions"
[imageUrl]="imageService.getCollectionCoverImage(item.id)"
(clicked)="loadCollection(item)"
(selection)="bulkSelectionService.handleCardSelection('collection', position, collections.length, $event)"
[selected]="bulkSelectionService.isCardSelected('collection', position)" [allowSelection]="true">
<app-card-detail-layout
[isLoading]="isLoading"
[items]="collections"
[filterOpen]="filterOpen"
[jumpBarKeys]="jumpbarKeys"
[trackByIdentity]="trackByIdentity"
>
<ng-template #cardItem let-item let-position="idx">
<app-card-item [title]="item.title" [entity]="item" [actions]="collectionTagActions"
[imageUrl]="imageService.getCollectionCoverImage(item.id)"
[linkUrl]="'/collections/' + item.id"
(clicked)="loadCollection(item)"
(selection)="bulkSelectionService.handleCardSelection('collection', position, collections.length, $event)"
[selected]="bulkSelectionService.isCardSelected('collection', position)" [allowSelection]="true">
<ng-template #subtitle>
<app-collection-owner [collection]="item"></app-collection-owner>
</ng-template>
</app-card-item>
</ng-template>
<ng-template #subtitle>
<app-collection-owner [collection]="item"></app-collection-owner>
</ng-template>
</app-card-item>
</ng-template>
<ng-template #noData>
{{t('no-data')}}
@if(accountService.isAdmin$ | async) {
{{t('create-one-part-1')}} <a [href]="WikiLink.Collections" rel="noopener noreferrer" target="_blank">{{t('create-one-part-2')}}<i class="fa fa-external-link-alt ms-1" aria-hidden="true"></i></a>
}
</ng-template>
</app-card-detail-layout>
<ng-template #noData>
{{t('no-data')}}
@if(accountService.isAdmin$ | async) {
{{t('create-one-part-1')}} <a [href]="WikiLink.Collections" rel="noopener noreferrer" target="_blank">{{t('create-one-part-2')}}<i class="fa fa-external-link-alt ms-1" aria-hidden="true"></i></a>
}
</ng-template>
</app-card-detail-layout>
</ng-container>
</ng-container>
</div>

View File

@ -0,0 +1,3 @@
.main-container {
margin-top: 10px;
}

View File

@ -1,74 +1,76 @@
<ng-container *transloco="let t; read: 'collection-detail'">
<div #companionBar>
<app-side-nav-companion-bar *ngIf="series !== undefined" [hasFilter]="true" (filterOpen)="filterOpen.emit($event)" [filterActive]="filterActive">
<ng-container title>
<h4 *ngIf="collectionTag !== undefined">
{{collectionTag.title}}<span class="ms-1" *ngIf="collectionTag.promoted">(<i aria-hidden="true" class="fa fa-angle-double-up"></i>)</span>
<app-card-actionables [disabled]="actionInProgress" (actionHandler)="performAction($event)" [actions]="collectionTagActions" [labelBy]="collectionTag.title" iconClass="fa-ellipsis-v"></app-card-actionables>
</h4>
</ng-container>
</app-side-nav-companion-bar>
</div>
<div class="main-container container-fluid">
<ng-container *transloco="let t; read: 'collection-detail'">
<div #companionBar>
<app-side-nav-companion-bar *ngIf="series !== undefined" [hasFilter]="true" (filterOpen)="filterOpen.emit($event)" [filterActive]="filterActive">
<ng-container title>
<h4 *ngIf="collectionTag !== undefined">
{{collectionTag.title}}<span class="ms-1" *ngIf="collectionTag.promoted">(<i aria-hidden="true" class="fa fa-angle-double-up"></i>)</span>
<app-card-actionables [disabled]="actionInProgress" (actionHandler)="performAction($event)" [actions]="collectionTagActions" [labelBy]="collectionTag.title" iconClass="fa-ellipsis-v"></app-card-actionables>
</h4>
</ng-container>
</app-side-nav-companion-bar>
</div>
<div [ngStyle]="{'height': ScrollingBlockHeight}" class="main-container container-fluid pt-2" *ngIf="collectionTag !== undefined" #scrollingBlock>
@if (summary.length > 0 || collectionTag.source !== ScrobbleProvider.Kavita) {
<div class="row mb-3">
<div class="col-md-2 col-xs-4 col-sm-6 d-none d-sm-block">
<app-image [styles]="{'max-width': '481px'}" [imageUrl]="imageService.getCollectionCoverImage(collectionTag.id)"></app-image>
@if (collectionTag.source !== ScrobbleProvider.Kavita && collectionTag.missingSeriesFromSource !== null
&& series.length !== collectionTag.totalSourceCount && collectionTag.totalSourceCount > 0) {
<div class="under-image">
<app-image [imageUrl]="collectionTag.source | providerImage"
width="16px" height="16px"
[ngbTooltip]="collectionTag.source | providerName" tabindex="0"></app-image>
<span class="ms-2 me-2">{{t('sync-progress', {title: series.length + ' / ' + collectionTag.totalSourceCount})}}</span>
<i class="fa-solid fa-question-circle" aria-hidden="true" [ngbTooltip]="t('last-sync', {date: collectionTag.lastSyncUtc | date: 'short' | defaultDate })"></i>
</div>
}
<div [ngStyle]="{'height': ScrollingBlockHeight}" class="main-container container-fluid pt-2" *ngIf="collectionTag !== undefined" #scrollingBlock>
@if (summary.length > 0 || collectionTag.source !== ScrobbleProvider.Kavita) {
<div class="row mb-3">
<div class="col-md-2 col-xs-4 col-sm-6 d-none d-sm-block">
<app-image [styles]="{'max-width': '481px'}" [imageUrl]="imageService.getCollectionCoverImage(collectionTag.id)"></app-image>
@if (collectionTag.source !== ScrobbleProvider.Kavita && collectionTag.missingSeriesFromSource !== null
&& series.length !== collectionTag.totalSourceCount && collectionTag.totalSourceCount > 0) {
<div class="under-image">
<app-image [imageUrl]="collectionTag.source | providerImage"
width="16px" height="16px"
[ngbTooltip]="collectionTag.source | providerName" tabindex="0"></app-image>
<span class="ms-2 me-2">{{t('sync-progress', {title: series.length + ' / ' + collectionTag.totalSourceCount})}}</span>
<i class="fa-solid fa-question-circle" aria-hidden="true" [ngbTooltip]="t('last-sync', {date: collectionTag.lastSyncUtc | date: 'short' | defaultDate })"></i>
</div>
}
</div>
<div class="col-md-10 col-xs-8 col-sm-6 mt-2">
@if (summary.length > 0) {
<div class="mb-2">
<app-read-more [text]="summary" [maxLength]="(utilityService.activeBreakpoint$ | async)! >= Breakpoint.Desktop ? 585 : 200"></app-read-more>
</div>
}
</div>
<hr>
</div>
<div class="col-md-10 col-xs-8 col-sm-6 mt-2">
@if (summary.length > 0) {
<div class="mb-2">
<app-read-more [text]="summary" [maxLength]="utilityService.getActiveBreakpoint() < Breakpoint.Tablet ? 250 : 600"></app-read-more>
</div>
}
}
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
<app-card-detail-layout *ngIf="filter"
[header]="t('series-header')"
[isLoading]="isLoading"
[items]="series"
[pagination]="pagination"
[filterSettings]="filterSettings"
[filterOpen]="filterOpen"
[parentScroll]="scrollingBlock"
[trackByIdentity]="trackByIdentity"
[jumpBarKeys]="jumpbarKeys"
(applyFilter)="updateFilter($event)">
<ng-template #cardItem let-item let-position="idx">
<app-series-card [series]="item" [libraryId]="item.libraryId" (reload)="loadPage()"
(selection)="bulkSelectionService.handleCardSelection('series', position, series.length, $event)" [selected]="bulkSelectionService.isCardSelected('series', position)" [allowSelection]="true"
></app-series-card>
</ng-template>
<div *ngIf="!filterActive && series.length === 0">
<ng-template #noData>
{{t('no-data')}}
</ng-template>
</div>
<hr>
</div>
}
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
<app-card-detail-layout *ngIf="filter"
[header]="t('series-header')"
[isLoading]="isLoading"
[items]="series"
[pagination]="pagination"
[filterSettings]="filterSettings"
[filterOpen]="filterOpen"
[parentScroll]="scrollingBlock"
[trackByIdentity]="trackByIdentity"
[jumpBarKeys]="jumpbarKeys"
(applyFilter)="updateFilter($event)">
<ng-template #cardItem let-item let-position="idx">
<app-series-card [series]="item" [libraryId]="item.libraryId" (reload)="loadPage()"
(selection)="bulkSelectionService.handleCardSelection('series', position, series.length, $event)" [selected]="bulkSelectionService.isCardSelected('series', position)" [allowSelection]="true"
></app-series-card>
</ng-template>
<div *ngIf="!filterActive && series.length === 0">
<ng-template #noData>
{{t('no-data')}}
</ng-template>
</div>
<div *ngIf="filterActive && series.length === 0">
<ng-template #noData>
{{t('no-data-filtered')}}
</ng-template>
</div>
</app-card-detail-layout>
</div>
</ng-container>
<div *ngIf="filterActive && series.length === 0">
<ng-template #noData>
{{t('no-data-filtered')}}
</ng-template>
</div>
</app-card-detail-layout>
</div>
</ng-container>
</div>

View File

@ -1,4 +1,4 @@
import {DatePipe, DOCUMENT, NgIf, NgStyle} from '@angular/common';
import {AsyncPipe, DatePipe, DOCUMENT, NgIf, NgStyle} from '@angular/common';
import {
AfterContentChecked,
ChangeDetectionStrategy,
@ -67,7 +67,7 @@ import {ProviderNamePipe} from "../../../_pipes/provider-name.pipe";
styleUrls: ['./collection-detail.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [NgIf, SideNavCompanionBarComponent, CardActionablesComponent, NgStyle, ImageComponent, ReadMoreComponent, BulkOperationsComponent, CardDetailLayoutComponent, SeriesCardComponent, TranslocoDirective, NgbTooltip, SafeHtmlPipe, TranslocoDatePipe, DatePipe, DefaultDatePipe, ProviderImagePipe, ProviderNamePipe]
imports: [NgIf, SideNavCompanionBarComponent, CardActionablesComponent, NgStyle, ImageComponent, ReadMoreComponent, BulkOperationsComponent, CardDetailLayoutComponent, SeriesCardComponent, TranslocoDirective, NgbTooltip, SafeHtmlPipe, TranslocoDatePipe, DatePipe, DefaultDatePipe, ProviderImagePipe, ProviderNamePipe, AsyncPipe]
})
export class CollectionDetailComponent implements OnInit, AfterContentChecked {

View File

@ -1,109 +1,104 @@
<div class="main-container">
<app-side-nav-companion-bar></app-side-nav-companion-bar>
<ng-container *transloco="let t; read: 'dashboard'">
@if (libraries$ | async; as libraries) {
@if (libraries.length === 0) {
@if (accountService.isAdmin$ | async; as isAdmin) {
<div class="mt-3">
@if (isAdmin) {
<div class="d-flex justify-content-center">
<p>{{t('no-libraries')}} <a routerLink="/settings" [fragment]="SettingsTabId.Libraries">{{t('server-settings-link')}}</a>.</p>
</div>
} @else {
<div class="d-flex justify-content-center">
<p>{{t('not-granted')}}</p>
</div>
}
</div>
}
}
}
@for(stream of streams; track stream.id) {
@switch (stream.streamType) {
@case (StreamType.OnDeck) {
<ng-container [ngTemplateOutlet]="onDeck" [ngTemplateOutletContext]="{ stream: stream }"></ng-container>
}
@case (StreamType.RecentlyUpdated) {
<ng-container [ngTemplateOutlet]="recentlyUpdated" [ngTemplateOutletContext]="{ stream: stream }"></ng-container>
}
@case (StreamType.NewlyAdded) {
<ng-container [ngTemplateOutlet]="newlyUpdated" [ngTemplateOutletContext]="{ stream: stream }"></ng-container>
}
@case (StreamType.SmartFilter) {
<ng-container [ngTemplateOutlet]="smartFilter" [ngTemplateOutletContext]="{ stream: stream }"></ng-container>
}
@case (StreamType.MoreInGenre) {
<ng-container [ngTemplateOutlet]="moreInGenre" [ngTemplateOutletContext]="{ stream: stream }"></ng-container>
<ng-container *transloco="let t; read: 'dashboard'">
@if (libraries$ | async; as libraries) {
@if (libraries.length === 0) {
@if (accountService.isAdmin$ | async; as isAdmin) {
<div class="mt-3">
@if (isAdmin) {
<div class="d-flex justify-content-center">
<p>{{t('no-libraries')}} <a routerLink="/settings" [fragment]="SettingsTabId.Libraries">{{t('server-settings-link')}}</a>.</p>
</div>
} @else {
<div class="d-flex justify-content-center">
<p>{{t('not-granted')}}</p>
</div>
}
</div>
}
}
}
<ng-template #smartFilter let-stream: DashboardStream>
@if(stream.api | async; as data) {
<app-carousel-reel [items]="data" [title]="stream.name" (sectionClick)="handleFilterSectionClick(stream)">
<ng-template #carouselItem let-item>
<app-series-card [series]="item" [libraryId]="item.libraryId"
(reload)="reloadStream(item.id)" (dataChanged)="reloadStream(item.id)"></app-series-card>
</ng-template>
</app-carousel-reel>
@for(stream of streams; track stream.id) {
@switch (stream.streamType) {
@case (StreamType.OnDeck) {
<ng-container [ngTemplateOutlet]="onDeck" [ngTemplateOutletContext]="{ stream: stream }"></ng-container>
}
@case (StreamType.RecentlyUpdated) {
<ng-container [ngTemplateOutlet]="recentlyUpdated" [ngTemplateOutletContext]="{ stream: stream }"></ng-container>
}
@case (StreamType.NewlyAdded) {
<ng-container [ngTemplateOutlet]="newlyUpdated" [ngTemplateOutletContext]="{ stream: stream }"></ng-container>
}
@case (StreamType.SmartFilter) {
<ng-container [ngTemplateOutlet]="smartFilter" [ngTemplateOutletContext]="{ stream: stream }"></ng-container>
}
@case (StreamType.MoreInGenre) {
<ng-container [ngTemplateOutlet]="moreInGenre" [ngTemplateOutletContext]="{ stream: stream }"></ng-container>
}
}
</ng-template>
<ng-template #onDeck let-stream: DashboardStream>
@if(stream.api | async; as data) {
<app-carousel-reel [items]="data" [title]="t('on-deck-title')" (sectionClick)="handleSectionClick(StreamId.OnDeck)">
<ng-template #carouselItem let-item>
<app-series-card [series]="item" [libraryId]="item.libraryId" [isOnDeck]="true"
(reload)="reloadStream(stream.id, true)" (dataChanged)="reloadStream(stream.id)"></app-series-card>
</ng-template>
</app-carousel-reel>
}
</ng-template>
<ng-template #recentlyUpdated let-stream: DashboardStream>
@if(stream.api | async; as data) {
<app-carousel-reel [items]="data" [title]="t('recently-updated-title')" (sectionClick)="handleSectionClick(StreamId.RecentlyUpdatedSeries)">
<ng-template #carouselItem let-item>
<app-card-item [entity]="item" [title]="item.seriesName" [imageUrl]="imageService.getSeriesCoverImage(item.seriesId)"
[suppressArchiveWarning]="true" (clicked)="handleRecentlyAddedChapterClick(item)" [count]="item.count"
[showReadButton]="true" (readClicked)="handleRecentlyAddedChapterRead(item)">
<ng-template #smartFilter let-stream: DashboardStream>
@if(stream.api | async; as data) {
<app-carousel-reel [items]="data" [title]="stream.name" (sectionClick)="handleFilterSectionClick(stream)">
<ng-template #carouselItem let-item>
<app-series-card [series]="item" [libraryId]="item.libraryId"
(reload)="reloadStream(item.id)" (dataChanged)="reloadStream(item.id)"></app-series-card>
</ng-template>
</app-carousel-reel>
}
</ng-template>
</app-card-item>
</ng-template>
</app-carousel-reel>
<ng-template #onDeck let-stream: DashboardStream>
@if(stream.api | async; as data) {
<app-carousel-reel [items]="data" [title]="t('on-deck-title')" (sectionClick)="handleSectionClick(StreamId.OnDeck)">
<ng-template #carouselItem let-item>
<app-series-card [series]="item" [libraryId]="item.libraryId" [isOnDeck]="true"
(reload)="reloadStream(stream.id, true)" (dataChanged)="reloadStream(stream.id)"></app-series-card>
</ng-template>
</app-carousel-reel>
}
</ng-template>
<ng-template #itemOverlay let-item="item">
<span (click)="handleRecentlyAddedChapterClick(item)">
<div>
<i class="fa-solid fa-book" aria-hidden="true"></i>
</div>
</span>
</ng-template>
}
</ng-template>
<ng-template #recentlyUpdated let-stream: DashboardStream>
@if(stream.api | async; as data) {
<app-carousel-reel [items]="data" [title]="t('recently-updated-title')" (sectionClick)="handleSectionClick(StreamId.RecentlyUpdatedSeries)">
<ng-template #carouselItem let-item>
<app-card-item [entity]="item" [title]="item.seriesName" [imageUrl]="imageService.getSeriesCoverImage(item.seriesId)"
[suppressArchiveWarning]="true" [count]="item.count" (clicked)="handleRecentlyAddedChapterClick(item)"
[showReadButton]="true" (readClicked)="handleRecentlyAddedChapterRead(item)"
[linkUrl]="'/library/' + item.libraryId + '/series/' + item.seriesId">
<ng-template #newlyUpdated let-stream: DashboardStream>
@if(stream.api | async; as data) {
<app-carousel-reel [items]="data" [title]="t('recently-added-title')" (sectionClick)="handleSectionClick(StreamId.NewlyAddedSeries)">
<ng-template #carouselItem let-item>
<app-series-card [series]="item" [libraryId]="item.libraryId" (dataChanged)="reloadStream(stream.id)"></app-series-card>
</ng-template>
</app-carousel-reel>
}
</ng-template>
</app-card-item>
</ng-template>
</app-carousel-reel>
}
</ng-template>
<ng-template #moreInGenre let-stream: DashboardStream>
@if(stream.api | async; as data) {
<app-carousel-reel [items]="data" [title]="t('more-in-genre-title', {genre: genre?.title})" (sectionClick)="handleSectionClick(StreamId.MoreInGenre)">
<ng-template #carouselItem let-item>
<app-series-card [series]="item" [libraryId]="item.libraryId" (dataChanged)="reloadStream(stream.id)"></app-series-card>
</ng-template>
</app-carousel-reel>
}
</ng-template>
}
<ng-template #newlyUpdated let-stream: DashboardStream>
@if(stream.api | async; as data) {
<app-carousel-reel [items]="data" [title]="t('recently-added-title')" (sectionClick)="handleSectionClick(StreamId.NewlyAddedSeries)">
<ng-template #carouselItem let-item>
<app-series-card [series]="item" [libraryId]="item.libraryId" (dataChanged)="reloadStream(stream.id)"></app-series-card>
</ng-template>
</app-carousel-reel>
}
</ng-template>
<app-loading [loading]="isLoadingDashboard || (streamCount !== streamsLoaded)"></app-loading>
</ng-container>
<ng-template #moreInGenre let-stream: DashboardStream>
@if(stream.api | async; as data) {
<app-carousel-reel [items]="data" [title]="t('more-in-genre-title', {genre: genre?.title})" (sectionClick)="handleSectionClick(StreamId.MoreInGenre)">
<ng-template #carouselItem let-item>
<app-series-card [series]="item" [libraryId]="item.libraryId" (dataChanged)="reloadStream(stream.id)"></app-series-card>
</ng-template>
</app-carousel-reel>
}
</ng-template>
}
<app-loading [loading]="isLoadingDashboard || (streamCount !== streamsLoaded)"></app-loading>
</ng-container>
</div>

View File

@ -0,0 +1,4 @@
.main-container {
margin-top: 10px;
padding: 0 0 0 10px;
}

View File

@ -1,7 +1,7 @@
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, OnInit} from '@angular/core';
import {Title} from '@angular/platform-browser';
import {Router, RouterLink} from '@angular/router';
import {Observable, of, ReplaySubject, Subject, switchMap} from 'rxjs';
import {Observable, ReplaySubject, Subject, switchMap} from 'rxjs';
import {debounceTime, map, shareReplay, take, tap, throttleTime} from 'rxjs/operators';
import {FilterUtilitiesService} from 'src/app/shared/_services/filter-utilities.service';
import {Library} from 'src/app/_models/library/library';
@ -32,7 +32,6 @@ import {StreamType} from "../../_models/dashboard/stream-type.enum";
import {LoadingComponent} from "../../shared/loading/loading.component";
import {ScrobbleProvider, ScrobblingService} from "../../_services/scrobbling.service";
import {ToastrService} from "ngx-toastr";
import {ServerService} from "../../_services/server.service";
import {SettingsTabId} from "../../sidenav/preference-nav/preference-nav.component";
import {ReaderService} from "../../_services/reader.service";

View File

@ -1,36 +1,38 @@
<ng-container *transloco="let t">
<app-side-nav-companion-bar [hasFilter]="true" (filterOpen)="filterOpen.emit($event)" [filterActive]="filterActive">
<h4 title>
<span>{{libraryName}}</span>
<app-card-actionables [actions]="actions" (actionHandler)="performAction($event)"></app-card-actionables>
</h4>
@if (active.fragment === '') {
<h5 subtitle class="subtitle-with-actionables">{{t('common.series-count', {num: pagination.totalItems | number})}} </h5>
<div class="main-container container-fluid">
<ng-container *transloco="let t">
<app-side-nav-companion-bar [hasFilter]="true" (filterOpen)="filterOpen.emit($event)" [filterActive]="filterActive">
<h4 title>
<span>{{libraryName}}</span>
<app-card-actionables [actions]="actions" (actionHandler)="performAction($event)"></app-card-actionables>
</h4>
@if (active.fragment === '') {
<h5 subtitle class="subtitle-with-actionables">{{t('common.series-count', {num: pagination.totalItems | number})}} </h5>
}
</app-side-nav-companion-bar>
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
<app-loading [absolute]="true" [loading]="bulkLoader"></app-loading>
@if (filter) {
<app-card-detail-layout
[isLoading]="loadingSeries"
[items]="series"
[pagination]="pagination"
[filterSettings]="filterSettings"
[trackByIdentity]="trackByIdentity"
[filterOpen]="filterOpen"
[jumpBarKeys]="jumpKeys"
[refresh]="refresh"
(applyFilter)="updateFilter($event)"
>
<ng-template #cardItem let-item let-position="idx">
<app-series-card [series]="item" [libraryId]="libraryId" [suppressLibraryLink]="true" (reload)="loadPage()"
(selection)="bulkSelectionService.handleCardSelection('series', position, series.length, $event)"
[selected]="bulkSelectionService.isCardSelected('series', position)" [allowSelection]="true"></app-series-card>
</ng-template>
</app-card-detail-layout>
}
</app-side-nav-companion-bar>
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
<app-loading [absolute]="true" [loading]="bulkLoader"></app-loading>
@if (filter) {
<app-card-detail-layout
[isLoading]="loadingSeries"
[items]="series"
[pagination]="pagination"
[filterSettings]="filterSettings"
[trackByIdentity]="trackByIdentity"
[filterOpen]="filterOpen"
[jumpBarKeys]="jumpKeys"
[refresh]="refresh"
(applyFilter)="updateFilter($event)"
>
<ng-template #cardItem let-item let-position="idx">
<app-series-card [series]="item" [libraryId]="libraryId" [suppressLibraryLink]="true" (reload)="loadPage()"
(selection)="bulkSelectionService.handleCardSelection('series', position, series.length, $event)"
[selected]="bulkSelectionService.isCardSelected('series', position)" [allowSelection]="true"></app-series-card>
</ng-template>
</app-card-detail-layout>
}
</ng-container>
</ng-container>
</div>

View File

@ -8,3 +8,8 @@
height: 100%;
overflow-y: auto;
}
.main-container {
margin-top: 10px;
}

View File

@ -31,7 +31,7 @@
>
<ng-template #libraryTemplate let-item>
<div style="display: flex;padding: 5px;" (click)="clickLibraryResult(item)">
<div class="clickable" style="display: flex;padding: 5px;" (click)="clickLibraryResult(item)">
<div class="ms-1">
<span>{{item.name}}</span>
</div>
@ -39,7 +39,7 @@
</ng-template>
<ng-template #seriesTemplate let-item>
<div style="display: flex;padding: 5px;" (click)="clickSeriesSearchResult(item)">
<div class="clickable" style="display: flex;padding: 5px;" (click)="clickSeriesSearchResult(item)">
<div style="width: 24px" class="me-1">
<app-image class="me-3 search-result" width="24px" [imageUrl]="imageService.getSeriesCoverImage(item.seriesId)"></app-image>
</div>
@ -58,7 +58,7 @@
</ng-template>
<ng-template #bookmarkTemplate let-item>
<div style="display: flex;padding: 5px;" (click)="clickBookmarkSearchResult(item)">
<div class="clickable" style="display: flex;padding: 5px;" (click)="clickBookmarkSearchResult(item)">
<div style="width: 24px" class="me-1">
<app-image class="me-3 search-result" width="24px" [imageUrl]="imageService.getSeriesCoverImage(item.seriesId)"></app-image>
</div>
@ -77,7 +77,7 @@
</ng-template>
<ng-template #collectionTemplate let-item>
<div style="display: flex;padding: 5px;" (click)="clickCollectionSearchResult(item)">
<div class="clickable" style="display: flex;padding: 5px;" (click)="clickCollectionSearchResult(item)">
<div style="width: 24px" class="me-1">
<app-image class="me-3 search-result" width="24px" [imageUrl]="imageService.getCollectionCoverImage(item.id)"></app-image>
</div>
@ -92,7 +92,7 @@
</ng-template>
<ng-template #readingListTemplate let-item>
<div style="display: flex;padding: 5px;" (click)="clickReadingListSearchResult(item)">
<div class="clickable" style="display: flex;padding: 5px;" (click)="clickReadingListSearchResult(item)">
<div class="ms-1">
<span>{{item.title}}</span>
<app-promoted-icon [promoted]="item.promoted"></app-promoted-icon>
@ -101,7 +101,7 @@
</ng-template>
<ng-template #tagTemplate let-item>
<div style="display: flex;padding: 5px;" (click)="goToOther(FilterField.Tags, item.id)">
<div class="clickable" style="display: flex;padding: 5px;" (click)="goToOther(FilterField.Tags, item.id)">
<div class="ms-1">
<span>{{item.title}}</span>
</div>
@ -141,7 +141,7 @@
</ng-template>
<ng-template #fileTemplate let-item>
<div style="display: flex;padding: 5px;" (click)="clickFileSearchResult(item)">
<div class="clickable" style="display: flex;padding: 5px;" (click)="clickFileSearchResult(item)">
<div class="ms-1">
<app-series-format [format]="item.format"></app-series-format>
<span>{{item.filePath}}</span>

View File

@ -1,6 +1,6 @@
import { Directive, ElementRef, EventEmitter, NgZone, OnDestroy, OnInit, Output } from '@angular/core';
import {Directive, ElementRef, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output} from '@angular/core';
import { Subscription } from 'rxjs';
import { createSwipeSubscription, SwipeEvent } from './ag-swipe.core';
import {createSwipeSubscription, SwipeDirection, SwipeEvent, SwipeStartEvent} from './ag-swipe.core';
@Directive({
selector: '[ngSwipe]',
@ -9,8 +9,13 @@ import { createSwipeSubscription, SwipeEvent } from './ag-swipe.core';
export class SwipeDirective implements OnInit, OnDestroy {
private swipeSubscription: Subscription | undefined;
@Input() restrictSwipeToLeftSide: boolean = false;
@Output() swipeMove: EventEmitter<SwipeEvent> = new EventEmitter<SwipeEvent>();
@Output() swipeEnd: EventEmitter<SwipeEvent> = new EventEmitter<SwipeEvent>();
@Output() swipeLeft: EventEmitter<void> = new EventEmitter<void>();
@Output() swipeRight: EventEmitter<void> = new EventEmitter<void>();
@Output() swipeUp: EventEmitter<void> = new EventEmitter<void>();
@Output() swipeDown: EventEmitter<void> = new EventEmitter<void>();
constructor(
private elementRef: ElementRef,
@ -22,12 +27,49 @@ export class SwipeDirective implements OnInit, OnDestroy {
this.swipeSubscription = createSwipeSubscription({
domElement: this.elementRef.nativeElement,
onSwipeMove: (swipeMoveEvent: SwipeEvent) => this.swipeMove.emit(swipeMoveEvent),
onSwipeEnd: (swipeEndEvent: SwipeEvent) => this.swipeEnd.emit(swipeEndEvent)
onSwipeEnd: (swipeEndEvent: SwipeEvent) => {
if (this.isSwipeWithinRestrictedArea(swipeEndEvent)) {
this.swipeEnd.emit(swipeEndEvent);
this.detectSwipeDirection(swipeEndEvent);
}
}
});
});
}
private isSwipeWithinRestrictedArea(swipeEvent: SwipeEvent): boolean {
if (!this.restrictSwipeToLeftSide) return true; // If restriction is disabled, allow all swipes
const elementRect = this.elementRef.nativeElement.getBoundingClientRect();
const touchAreaWidth = elementRect.width * 0.3; // Define the left area (30% of the element's width)
// Assuming swipeEvent includes the starting coordinates; you may need to adjust this logic
if (swipeEvent.direction === SwipeDirection.X && Math.abs(swipeEvent.distance) < touchAreaWidth) {
return true;
}
return false;
}
private detectSwipeDirection(swipeEvent: SwipeEvent) {
if (swipeEvent.direction === SwipeDirection.X) {
if (swipeEvent.distance > 0) {
this.swipeRight.emit();
} else {
this.swipeLeft.emit();
}
} else if (swipeEvent.direction === SwipeDirection.Y) {
if (swipeEvent.distance > 0) {
this.swipeDown.emit();
} else {
this.swipeUp.emit();
}
}
}
ngOnDestroy() {
this.swipeSubscription?.unsubscribe?.();
this.swipeSubscription?.unsubscribe();
}
}

View File

@ -1,166 +1,168 @@
<ng-container *transloco="let t; read: 'reading-list-detail'">
<app-side-nav-companion-bar [hasExtras]="readingList !== undefined" [extraDrawer]="extrasDrawer">
<h4 title>
{{readingList?.title}}
@if (readingList?.promoted) {
<span class="ms-1">(<i class="fa fa-angle-double-up" aria-hidden="true"></i>)</span>
}
@if (actions.length > 0) {
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [attr.aria-labelledby]="readingList?.title"></app-card-actionables>
}
</h4>
<h5 subtitle class="subtitle-with-actionables">{{t('item-count', {num: items.length | number})}}</h5>
<div class="main-container container-fluid">
<ng-container *transloco="let t; read: 'reading-list-detail'">
<app-side-nav-companion-bar [hasExtras]="readingList !== undefined" [extraDrawer]="extrasDrawer">
<h4 title>
{{readingList?.title}}
@if (readingList?.promoted) {
<span class="ms-1">(<i class="fa fa-angle-double-up" aria-hidden="true"></i>)</span>
}
@if (actions.length > 0) {
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [attr.aria-labelledby]="readingList?.title"></app-card-actionables>
}
</h4>
<h5 subtitle class="subtitle-with-actionables">{{t('item-count', {num: items.length | number})}}</h5>
<ng-template #extrasDrawer let-offcanvas>
@if (readingList) {
<div>
<div class="offcanvas-header">
<h4 class="offcanvas-title" id="offcanvas-basic-title">{{t('page-settings-title')}}</h4>
<button type="button" class="btn-close" aria-label="Close" (click)="offcanvas.dismiss()"></button>
<ng-template #extrasDrawer let-offcanvas>
@if (readingList) {
<div>
<div class="offcanvas-header">
<h4 class="offcanvas-title" id="offcanvas-basic-title">{{t('page-settings-title')}}</h4>
<button type="button" class="btn-close" aria-label="Close" (click)="offcanvas.dismiss()"></button>
</div>
<div class="offcanvas-body">
<div class="row g-0">
<div class="col-md-12 col-sm-12 pe-2 mb-3">
<button class="btn btn-danger" (click)="removeRead()" [disabled]="readingList.promoted && !this.isAdmin">
<span>
<i class="fa fa-check"></i>
</span>
<span class="read-btn--text">&nbsp;{{t('remove-read')}}</span>
</button>
@if (!(readingList.promoted && !this.isAdmin)) {
<div class="col-auto ms-2 mt-2">
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="accessibility-mode" [disabled]="this.utilityService.getActiveBreakpoint() < Breakpoint.Tablet" [value]="accessibilityMode" (change)="updateAccessibilityMode()">
<label class="form-check-label" for="accessibility-mode">{{t('order-numbers-label')}}</label>
</div>
</div>
}
</div>
</div>
</div>
</div>
<div class="offcanvas-body">
<div class="row g-0">
<div class="col-md-12 col-sm-12 pe-2 mb-3">
<button class="btn btn-danger" (click)="removeRead()" [disabled]="readingList.promoted && !this.isAdmin">
<span>
<i class="fa fa-check"></i>
</span>
<span class="read-btn--text">&nbsp;{{t('remove-read')}}</span>
</button>
}
</ng-template>
</app-side-nav-companion-bar>
@if (!(readingList.promoted && !this.isAdmin)) {
<div class="col-auto ms-2 mt-2">
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="accessibility-mode" [disabled]="this.utilityService.getActiveBreakpoint() < Breakpoint.Tablet" [value]="accessibilityMode" (change)="updateAccessibilityMode()">
<label class="form-check-label" for="accessibility-mode">{{t('order-numbers-label')}}</label>
@if (readingList) {
<div class="container-fluid mt-2">
<div class="row mb-2">
<div class="col-md-2 col-xs-4 col-sm-6 d-none d-sm-block">
<app-image [styles]="{'max-height': '400px', 'max-width': '300px'}" [imageUrl]="imageService.getReadingListCoverImage(readingList.id)"></app-image>
</div>
<div class="col-md-10 col-xs-8 col-sm-6 mt-2">
<div class="row g-0 mb-3">
<div class="col-auto me-2">
<!-- Action row-->
<div class="btn-group me-3">
<button type="button" class="btn btn-primary" (click)="continue()">
<span>
<i class="fa fa-book-open me-1" aria-hidden="true"></i>
<span class="read-btn--text">{{t('continue')}}</span>
</span>
</button>
<div class="btn-group" ngbDropdown role="group" [attr.aria-label]="t('read-options-alt')">
<button type="button" class="btn btn-primary dropdown-toggle-split" ngbDropdownToggle></button>
<div class="dropdown-menu" ngbDropdownMenu>
<button ngbDropdownItem (click)="read()">
<span>
<i class="fa fa-book" aria-hidden="true"></i>
<span class="read-btn--text">&nbsp;{{t('read')}}</span>
</span>
</button>
<button ngbDropdownItem (click)="continue(true)">
<span>
<i class="fa fa-book-open me-1" aria-hidden="true"></i>
<span class="read-btn--text">{{t('continue')}}</span>
(<i class="fa fa-glasses ms-1" aria-hidden="true"></i>)
<span class="visually-hidden">{{t('incognito-alt')}}</span>
</span>
</button>
<button ngbDropdownItem (click)="read(true)">
<span>
<i class="fa fa-book me-1" aria-hidden="true"></i>
<span class="read-btn--text">&nbsp;{{t('read')}}</span>
(<i class="fa fa-glasses ms-1" aria-hidden="true"></i>)
<span class="visually-hidden">{{t('incognito-alt')}}</span>
</span>
</button>
</div>
</div>
}
</div>
</div>
</div>
</div>
}
</ng-template>
</app-side-nav-companion-bar>
@if (readingList) {
<div class="container-fluid mt-2">
<div class="row mb-2">
<div class="col-md-2 col-xs-4 col-sm-6 d-none d-sm-block">
<app-image [styles]="{'max-height': '400px', 'max-width': '300px'}" [imageUrl]="imageService.getReadingListCoverImage(readingList.id)"></app-image>
</div>
<div class="col-md-10 col-xs-8 col-sm-6 mt-2">
<div class="row g-0 mb-3">
<div class="col-auto me-2">
<!-- Action row-->
<div class="btn-group me-3">
<button type="button" class="btn btn-primary" (click)="continue()">
<span>
<i class="fa fa-book-open me-1" aria-hidden="true"></i>
<span class="read-btn--text">{{t('continue')}}</span>
</span>
</button>
<div class="btn-group" ngbDropdown role="group" [attr.aria-label]="t('read-options-alt')">
<button type="button" class="btn btn-primary dropdown-toggle-split" ngbDropdownToggle></button>
<div class="dropdown-menu" ngbDropdownMenu>
<button ngbDropdownItem (click)="read()">
<span>
<i class="fa fa-book" aria-hidden="true"></i>
<span class="read-btn--text">&nbsp;{{t('read')}}</span>
</span>
</button>
<button ngbDropdownItem (click)="continue(true)">
<span>
<i class="fa fa-book-open me-1" aria-hidden="true"></i>
<span class="read-btn--text">{{t('continue')}}</span>
(<i class="fa fa-glasses ms-1" aria-hidden="true"></i>)
<span class="visually-hidden">{{t('incognito-alt')}}</span>
</span>
</button>
<button ngbDropdownItem (click)="read(true)">
<span>
<i class="fa fa-book me-1" aria-hidden="true"></i>
<span class="read-btn--text">&nbsp;{{t('read')}}</span>
(<i class="fa fa-glasses ms-1" aria-hidden="true"></i>)
<span class="visually-hidden">{{t('incognito-alt')}}</span>
</span>
</button>
</div>
</div>
</div>
</div>
</div>
@if (readingList.startingYear !== 0) {
<div class="row g-0 mt-2">
<h4 class="reading-list-years">
@if (readingList.startingMonth > 0) {
{{(readingList.startingMonth +'/01/2020')| date:'MMM'}}
}
@if (readingList.startingMonth > 0 && readingList.startingYear > 0) {
,
}
@if (readingList.startingYear > 0) {
{{readingList.startingYear}}
}
@if (readingList.endingYear > 0) {
@if (readingList.endingMonth > 0) {
{{(readingList.endingMonth +'/01/2020')| date:'MMM'}}
@if (readingList.startingYear !== 0) {
<div class="row g-0 mt-2">
<h4 class="reading-list-years">
@if (readingList.startingMonth > 0) {
{{(readingList.startingMonth +'/01/2020')| date:'MMM'}}
}
@if (readingList.endingMonth > 0 && readingList.endingYear > 0) {
@if (readingList.startingMonth > 0 && readingList.startingYear > 0) {
,
}
@if (readingList.endingYear > 0) {
{{readingList.endingYear}}
@if (readingList.startingYear > 0) {
{{readingList.startingYear}}
}
}
</h4>
</div>
}
<!-- Summary row-->
<div class="row g-0 mt-2">
<app-read-more [text]="readingListSummary" [maxLength]="250"></app-read-more>
</div>
@if (characters$ | async; as characters) {
@if (characters && characters.length > 0) {
<div class="row mb-2">
<div class="row">
<h5>{{t('characters-title')}}</h5>
<app-badge-expander [items]="characters">
<ng-template #badgeExpanderItem let-item let-position="idx">
<a href="javascript:void(0)" class="dark-exempt btn-icon" (click)="goToCharacter(item)">{{item.name}}</a>
</ng-template>
</app-badge-expander>
</div>
@if (readingList.endingYear > 0) {
@if (readingList.endingMonth > 0) {
{{(readingList.endingMonth +'/01/2020')| date:'MMM'}}
}
@if (readingList.endingMonth > 0 && readingList.endingYear > 0) {
,
}
@if (readingList.endingYear > 0) {
{{readingList.endingYear}}
}
}
</h4>
</div>
}
<!-- Summary row-->
<div class="row g-0 mt-2">
<app-read-more [text]="readingListSummary" [maxLength]="(utilityService.activeBreakpoint$ | async)! >= Breakpoint.Desktop ? 585 : 200"></app-read-more>
</div>
@if (characters$ | async; as characters) {
@if (characters && characters.length > 0) {
<div class="row mb-2">
<div class="row">
<h5>{{t('characters-title')}}</h5>
<app-badge-expander [items]="characters">
<ng-template #badgeExpanderItem let-item let-position="idx">
<a href="javascript:void(0)" class="dark-exempt btn-icon" (click)="goToCharacter(item)">{{item.name}}</a>
</ng-template>
</app-badge-expander>
</div>
</div>
}
}
</div>
</div>
<div class="row mb-1 scroll-container" #scrollingBlock>
@if (items.length === 0 && !isLoading) {
<div class="mx-auto" style="width: 200px;">
{{t('no-data')}}
</div>
} @else if(isLoading) {
<app-loading [loading]="isLoading"></app-loading>
}
<app-draggable-ordered-list [items]="items" (orderUpdated)="orderUpdated($event)" [accessibilityMode]="accessibilityMode"
[showRemoveButton]="false">
<ng-template #draggableItem let-item let-position="idx">
<app-reading-list-item [ngClass]="{'content-container': items.length < 100, 'non-virtualized-container': items.length >= 100}" [item]="item" [position]="position" [libraryTypes]="libraryTypes"
[promoted]="item.promoted" (read)="readChapter($event)" (remove)="itemRemoved($event, position)"></app-reading-list-item>
</ng-template>
</app-draggable-ordered-list>
</div>
</div>
<div class="row mb-1 scroll-container" #scrollingBlock>
@if (items.length === 0 && !isLoading) {
<div class="mx-auto" style="width: 200px;">
{{t('no-data')}}
</div>
} @else if(isLoading) {
<app-loading [loading]="isLoading"></app-loading>
}
<app-draggable-ordered-list [items]="items" (orderUpdated)="orderUpdated($event)" [accessibilityMode]="accessibilityMode"
[showRemoveButton]="false">
<ng-template #draggableItem let-item let-position="idx">
<app-reading-list-item [ngClass]="{'content-container': items.length < 100, 'non-virtualized-container': items.length >= 100}" [item]="item" [position]="position" [libraryTypes]="libraryTypes"
[promoted]="item.promoted" (read)="readChapter($event)" (remove)="itemRemoved($event, position)"></app-reading-list-item>
</ng-template>
</app-draggable-ordered-list>
</div>
</div>
}
</ng-container>
}
</ng-container>
</div>

View File

@ -1,35 +1,38 @@
<ng-container *transloco="let t; read: 'reading-lists'">
<app-side-nav-companion-bar>
<h4 title>
<span>{{t('title')}}</span>
<app-card-actionables [actions]="globalActions" (actionHandler)="performGlobalAction($event)"></app-card-actionables>
</h4>
@if (pagination) {
<h5 subtitle class="subtitle-with-actionables">{{t('item-count', {num: pagination.totalItems | number})}}</h5>
}
<div class="main-container container-fluid">
<ng-container *transloco="let t; read: 'reading-lists'">
<app-side-nav-companion-bar>
<h4 title>
<span>{{t('title')}}</span>
<app-card-actionables [actions]="globalActions" (actionHandler)="performGlobalAction($event)"></app-card-actionables>
</h4>
@if (pagination) {
<h5 subtitle class="subtitle-with-actionables">{{t('item-count', {num: pagination.totalItems | number})}}</h5>
}
</app-side-nav-companion-bar>
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
</app-side-nav-companion-bar>
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
<app-card-detail-layout
[isLoading]="loadingLists"
[items]="lists"
[pagination]="pagination"
[jumpBarKeys]="jumpbarKeys"
[filteringDisabled]="true"
[trackByIdentity]="trackByIdentity"
>
<ng-template #cardItem let-item let-position="idx" >
<app-card-item [title]="item.title" [entity]="item" [actions]="actions[item.id]"
[suppressLibraryLink]="true" [imageUrl]="imageService.getReadingListCoverImage(item.id)"
(clicked)="handleClick(item)"
(selection)="bulkSelectionService.handleCardSelection('readingList', position, lists.length, $event)"
[selected]="bulkSelectionService.isCardSelected('readingList', position)" [allowSelection]="true"></app-card-item>
</ng-template>
<app-card-detail-layout
[isLoading]="loadingLists"
[items]="lists"
[pagination]="pagination"
[jumpBarKeys]="jumpbarKeys"
[filteringDisabled]="true"
[trackByIdentity]="trackByIdentity"
>
<ng-template #cardItem let-item let-position="idx" >
<app-card-item [title]="item.title" [entity]="item" [actions]="actions[item.id]"
[suppressLibraryLink]="true" [imageUrl]="imageService.getReadingListCoverImage(item.id)"
[linkUrl]="'/lists/' + item.id"
(clicked)="handleClick(item)"
(selection)="bulkSelectionService.handleCardSelection('readingList', position, lists.length, $event)"
[selected]="bulkSelectionService.isCardSelected('readingList', position)" [allowSelection]="true"></app-card-item>
</ng-template>
<ng-template #noData>
{{t('no-data')}} {{t('create-one-part-1')}} <a [href]="WikiLink.ReadingLists" rel="noopener noreferrer" target="_blank">{{t('create-one-part-2')}}<i class="fa fa-external-link-alt ms-1" aria-hidden="true"></i></a>.
</ng-template>
</app-card-detail-layout>
<ng-template #noData>
{{t('no-data')}} {{t('create-one-part-1')}} <a [href]="WikiLink.ReadingLists" rel="noopener noreferrer" target="_blank">{{t('create-one-part-2')}}<i class="fa fa-external-link-alt ms-1" aria-hidden="true"></i></a>.
</ng-template>
</app-card-detail-layout>
</ng-container>
</ng-container>
</div>

View File

@ -89,18 +89,16 @@ export class ReadingListsComponent implements OnInit {
.filter(action => this.readingListService.actionListFilter(action, readingList, this.isAdmin || this.hasPromote));
}
performAction(action: ActionItem<ReadingList>, readingList: ReadingList) {
if (typeof action.callback === 'function') {
action.callback(action, readingList);
}
}
performGlobalAction(action: ActionItem<any>) {
if (typeof action.callback === 'function') {
action.callback(action, undefined);
}
}
handleClick(list: ReadingList) {
this.router.navigateByUrl('lists/' + list.id);
}
handleReadingListActionCallback(action: ActionItem<ReadingList>, readingList: ReadingList) {
switch(action.action) {
case Action.Delete:
@ -159,10 +157,6 @@ export class ReadingListsComponent implements OnInit {
});
}
handleClick(list: ReadingList) {
this.router.navigateByUrl('lists/' + list.id);
}
bulkActionCallback = (action: ActionItem<any>, data: any) => {
const selectedReadingListIndexies = this.bulkSelectionService.getSelectedCardsForSource('readingList');
const selectedReadingLists = this.lists.filter((col, index: number) => selectedReadingListIndexies.includes(index + ''));

View File

@ -334,7 +334,7 @@
</li>
@if (seriesMetadata && showDetailsTab) {
<li [ngbNavItem]="TabID.Details">
<li [ngbNavItem]="TabID.Details" id="details-tab">
<a ngbNavLink>{{t(TabID.Details)}}</a>
<ng-template ngbNavContent>
@defer (when activeTabId === TabID.Details; prefetch on idle) {

View File

@ -1,13 +1,11 @@
@use '../../../../series-detail-common';
.to-read-counter {
position: absolute;
top: 15px;
left: 20px;
}
.card-container{
display: grid;
grid-template-columns: repeat(auto-fill, 160px);

View File

@ -1174,5 +1174,11 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
switchTabsToDetail() {
this.activeTabId = TabID.Details;
this.cdRef.markForCheck();
setTimeout(() => {
const tabElem = this.document.querySelector('#details-tab');
if (tabElem) {
(tabElem as HTMLLIElement).scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
}
}, 10);
}
}

View File

@ -1,180 +1,183 @@
<ng-container *transloco="let t; read:'settings'">
<app-side-nav-companion-bar>
<h2 title>
{{fragment | settingFragment}}
</h2>
</app-side-nav-companion-bar>
<div class="row col-me-4 pb-3">
@if (accountService.currentUser$ | async; as user) {
@if (accountService.hasAdminRole(user)) {
@defer (when fragment === SettingsTabId.General; prefetch on idle) {
@if (fragment === SettingsTabId.General) {
<div class="main-container container-fluid">
<ng-container *transloco="let t; read:'settings'">
<app-side-nav-companion-bar>
<h2 title>
{{fragment | settingFragment}}
</h2>
</app-side-nav-companion-bar>
<div class="row col-me-4 pb-3">
@if (accountService.currentUser$ | async; as user) {
@if (accountService.hasAdminRole(user)) {
@defer (when fragment === SettingsTabId.General; prefetch on idle) {
@if (fragment === SettingsTabId.General) {
<div class="col-xxl-6 col-12">
<app-manage-settings></app-manage-settings>
</div>
}
}
@defer (when fragment === SettingsTabId.Email; prefetch on idle) {
@if (fragment === SettingsTabId.Email) {
<div class="col-xxl-6 col-12">
<app-manage-email-settings></app-manage-email-settings>
</div>
}
}
@defer (when fragment === SettingsTabId.Media; prefetch on idle) {
@if (fragment === SettingsTabId.Media) {
<div class="col-xxl-6 col-12">
<app-manage-media-settings></app-manage-media-settings>
</div>
}
}
@defer (when fragment === SettingsTabId.Users; prefetch on idle) {
@if (fragment === SettingsTabId.Users) {
<div class="scale col-md-12">
<app-manage-users></app-manage-users>
</div>
}
}
@defer (when fragment === SettingsTabId.Libraries; prefetch on idle) {
@if (fragment === SettingsTabId.Libraries) {
<div class="scale col-md-12">
<app-manage-library></app-manage-library>
</div>
}
}
@defer (when fragment === SettingsTabId.MediaIssues; prefetch on idle) {
@if (fragment === SettingsTabId.MediaIssues) {
<div class="scale col-md-12">
<app-manage-media-issues></app-manage-media-issues>
</div>
}
}
@defer (when fragment === SettingsTabId.System; prefetch on idle) {
@if (fragment === SettingsTabId.System) {
<div class="scale col-md-12">
<app-manage-system></app-manage-system>
</div>
}
}
@defer (when fragment === SettingsTabId.Statistics; prefetch on idle) {
@if (fragment === SettingsTabId.Statistics) {
<div class="scale col-md-12">
<app-server-stats></app-server-stats>
</div>
}
}
@defer (when fragment === SettingsTabId.Tasks; prefetch on idle) {
@if (fragment === SettingsTabId.Tasks) {
<div class="scale col-md-12">
<app-manage-tasks-settings></app-manage-tasks-settings>
</div>
}
}
@defer (when fragment === SettingsTabId.KavitaPlus; prefetch on idle) {
@if (fragment === SettingsTabId.KavitaPlus) {
<div class="scale col-md-12">
<app-manage-kavitaplus></app-manage-kavitaplus>
</div>
}
}
}
@defer (when fragment === SettingsTabId.Account; prefetch on idle) {
@if (fragment === SettingsTabId.Account) {
<div class="col-xxl-6 col-12">
<app-manage-settings></app-manage-settings>
<app-change-email></app-change-email>
<div class="setting-section-break"></div>
<app-change-password></app-change-password>
<div class="setting-section-break"></div>
<app-change-age-restriction></app-change-age-restriction>
<div class="setting-section-break"></div>
<app-manage-scrobbling-providers></app-manage-scrobbling-providers>
</div>
}
}
@defer (when fragment === SettingsTabId.Email; prefetch on idle) {
@if (fragment === SettingsTabId.Email) {
@defer (when fragment === SettingsTabId.Preferences; prefetch on idle) {
@if (fragment === SettingsTabId.Preferences) {
<div class="col-xxl-6 col-12">
<app-manage-email-settings></app-manage-email-settings>
<app-manga-user-preferences></app-manga-user-preferences>
</div>
}
}
@defer (when fragment === SettingsTabId.Media; prefetch on idle) {
@if (fragment === SettingsTabId.Media) {
@defer (when fragment === SettingsTabId.Customize; prefetch on idle) {
@if (fragment === SettingsTabId.Customize) {
<div class="scale col-md-12">
<app-manage-customization></app-manage-customization>
</div>
}
}
@defer (when fragment === SettingsTabId.Clients; prefetch on idle) {
@if (fragment === SettingsTabId.Clients) {
<div class="col-xxl-6 col-12">
<app-manage-media-settings></app-manage-media-settings>
<app-manage-opds></app-manage-opds>
</div>
}
}
@defer (when fragment === SettingsTabId.Users; prefetch on idle) {
@if (fragment === SettingsTabId.Users) {
@defer (when fragment === SettingsTabId.Theme; prefetch on idle) {
@if (fragment === SettingsTabId.Theme) {
<div class="scale col-md-12">
<app-manage-users></app-manage-users>
<app-theme-manager></app-theme-manager>
</div>
}
}
@defer (when fragment === SettingsTabId.Libraries; prefetch on idle) {
@if (fragment === SettingsTabId.Libraries) {
@defer (when fragment === SettingsTabId.Devices; prefetch on idle) {
@if (fragment === SettingsTabId.Devices) {
<div class="scale col-md-12">
<app-manage-library></app-manage-library>
<app-manage-devices></app-manage-devices>
</div>
}
}
@defer (when fragment === SettingsTabId.MediaIssues; prefetch on idle) {
@if (fragment === SettingsTabId.MediaIssues) {
@defer (when fragment === SettingsTabId.UserStats; prefetch on idle) {
@if (fragment === SettingsTabId.UserStats) {
<div class="scale col-md-12">
<app-manage-media-issues></app-manage-media-issues>
<app-user-stats></app-user-stats>
</div>
}
}
@defer (when fragment === SettingsTabId.System; prefetch on idle) {
@if (fragment === SettingsTabId.System) {
@defer (when fragment === SettingsTabId.CBLImport; prefetch on idle) {
@if (fragment === SettingsTabId.CBLImport) {
<div class="scale col-md-12">
<app-manage-system></app-manage-system>
<app-import-cbl></app-import-cbl>
</div>
}
}
@defer (when fragment === SettingsTabId.Statistics; prefetch on idle) {
@if (fragment === SettingsTabId.Statistics) {
@defer (when fragment === SettingsTabId.Scrobbling; prefetch on idle) {
@if(hasActiveLicense && fragment === SettingsTabId.Scrobbling) {
<div class="scale col-md-12">
<app-server-stats></app-server-stats>
<app-manage-scrobling></app-manage-scrobling>
</div>
}
}
@defer (when fragment === SettingsTabId.Tasks; prefetch on idle) {
@if (fragment === SettingsTabId.Tasks) {
@defer (when fragment === SettingsTabId.MALStackImport; prefetch on idle) {
@if(hasActiveLicense && fragment === SettingsTabId.MALStackImport) {
<div class="scale col-md-12">
<app-manage-tasks-settings></app-manage-tasks-settings>
</div>
}
}
@defer (when fragment === SettingsTabId.KavitaPlus; prefetch on idle) {
@if (fragment === SettingsTabId.KavitaPlus) {
<div class="scale col-md-12">
<app-manage-kavitaplus></app-manage-kavitaplus>
<app-import-mal-collection></app-import-mal-collection>
</div>
}
}
}
</div>
</ng-container>
</div>
@defer (when fragment === SettingsTabId.Account; prefetch on idle) {
@if (fragment === SettingsTabId.Account) {
<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>
<div class="setting-section-break"></div>
<app-change-age-restriction></app-change-age-restriction>
<div class="setting-section-break"></div>
<app-manage-scrobbling-providers></app-manage-scrobbling-providers>
</div>
}
}
@defer (when fragment === SettingsTabId.Preferences; prefetch on idle) {
@if (fragment === SettingsTabId.Preferences) {
<div class="col-xxl-6 col-12">
<app-manga-user-preferences></app-manga-user-preferences>
</div>
}
}
@defer (when fragment === SettingsTabId.Customize; prefetch on idle) {
@if (fragment === SettingsTabId.Customize) {
<div class="scale col-md-12">
<app-manage-customization></app-manage-customization>
</div>
}
}
@defer (when fragment === SettingsTabId.Clients; prefetch on idle) {
@if (fragment === SettingsTabId.Clients) {
<div class="col-xxl-6 col-12">
<app-manage-opds></app-manage-opds>
</div>
}
}
@defer (when fragment === SettingsTabId.Theme; prefetch on idle) {
@if (fragment === SettingsTabId.Theme) {
<div class="scale col-md-12">
<app-theme-manager></app-theme-manager>
</div>
}
}
@defer (when fragment === SettingsTabId.Devices; prefetch on idle) {
@if (fragment === SettingsTabId.Devices) {
<div class="scale col-md-12">
<app-manage-devices></app-manage-devices>
</div>
}
}
@defer (when fragment === SettingsTabId.UserStats; prefetch on idle) {
@if (fragment === SettingsTabId.UserStats) {
<div class="scale col-md-12">
<app-user-stats></app-user-stats>
</div>
}
}
@defer (when fragment === SettingsTabId.CBLImport; prefetch on idle) {
@if (fragment === SettingsTabId.CBLImport) {
<div class="scale col-md-12">
<app-import-cbl></app-import-cbl>
</div>
}
}
@defer (when fragment === SettingsTabId.Scrobbling; prefetch on idle) {
@if(hasActiveLicense && fragment === SettingsTabId.Scrobbling) {
<div class="scale col-md-12">
<app-manage-scrobling></app-manage-scrobling>
</div>
}
}
@defer (when fragment === SettingsTabId.MALStackImport; prefetch on idle) {
@if(hasActiveLicense && fragment === SettingsTabId.MALStackImport) {
<div class="scale col-md-12">
<app-import-mal-collection></app-import-mal-collection>
</div>
}
}
}
</div>
</ng-container>

View File

@ -1,10 +1,28 @@
@import '../../../../theme/variables';
h2 {
color: white;
font-weight: bold;
}
.main-container {
margin-top: 10px;
}
::ng-deep .content-wrapper:not(.closed) {
.scale {
width: calc(100dvw - 200px) !important;
}
}
@media (max-width: $grid-breakpoints-lg) {
::ng-deep .content-wrapper:not(.closed) {
.scale {
width: 100% !important;
overflow-x: auto;
}
}
}

View File

@ -1,32 +1,31 @@
<ng-container *transloco="let t; read: 'customize-sidenav-streams'">
<form [formGroup]="listForm">
<div class="row g-0 mb-3 justify-content-between">
@if (items.length > 3) {
<div class="col-9">
<label for="sidenav-stream-filter" class="form-label">{{t('filter')}}</label>
<div class="input-group">
<input id="sidenav-stream-filter" autocomplete="off" class="form-control" formControlName="filterSideNavStream" type="text" aria-describedby="reset-sidenav-stream-input">
<button class="btn btn-outline-secondary" type="button" id="reset-sidenav-stream-input" (click)="resetSideNavFilter()">{{t('clear')}}</button>
</div>
@if (listForm.get('filterSideNavStream')?.value) {
<p role="alert" class="mt-2">{{t('reorder-when-filter-present')}}</p>
}
@if (items.length > 3) {
<div class="row g-0 mb-2">
<label for="sidenav-stream-filter" class="form-label">{{t('filter')}}</label>
<div class="input-group">
<input id="sidenav-stream-filter" autocomplete="off" class="form-control" formControlName="filterSideNavStream" type="text" aria-describedby="reset-sidenav-stream-input">
<button class="btn btn-outline-secondary" type="button" id="reset-sidenav-stream-input" (click)="resetSideNavFilter()">{{t('clear')}}</button>
</div>
}
<div class="col-3">
<form [formGroup]="pageOperationsForm">
<div class="form-check form-check-inline" style="margin-top: 40px; margin-left: 10px">
<input class="form-check-input" type="checkbox" id="accessibility-mode" formControlName="accessibilityMode">
<label class="form-check-label" for="accessibility-mode">{{t('order-numbers-label')}}</label>
</div>
<div class="form-check form-check-inline" style="margin-left: 10px">
<input class="form-check-input" type="checkbox" id="bulk-mode" formControlName="bulkMode" >
<label class="form-check-label" for="bulk-mode">{{t('bulk-mode-label')}}</label>
</div>
</form>
@if (listForm.get('filterSideNavStream')?.value) {
<p role="alert" class="mt-2">{{t('reorder-when-filter-present')}}</p>
}
</div>
}
<div class="row g-0 mb-3 ">
<form [formGroup]="pageOperationsForm">
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="accessibility-mode" formControlName="accessibilityMode">
<label class="form-check-label" for="accessibility-mode">{{t('order-numbers-label')}}</label>
</div>
<div class="form-check form-check-inline ms-2">
<input class="form-check-input" type="checkbox" id="bulk-mode" formControlName="bulkMode" >
<label class="form-check-label" for="bulk-mode">{{t('bulk-mode-label')}}</label>
</div>
</form>
</div>
<app-bulk-operations [modalMode]="true" [topOffset]="0" [actionCallback]="bulkActionCallback"></app-bulk-operations>
<div style="max-height: 500px; overflow-y: auto">
<app-draggable-ordered-list [items]="items | filter: filterSideNavStreams" (orderUpdated)="orderUpdated($event)"

View File

@ -1,3 +1,5 @@
@import "../../../../theme/variables";
.list-item {
height: 60px;
max-height: 60px;
@ -5,4 +7,10 @@
.meta {
display: flex;
}
@media (max-width: $grid-breakpoints-sm) {
.list-item div h5 span:first-of-type {
font-size: 1rem !important;
}
}

View File

@ -22,16 +22,23 @@
align-items: center;
display: flex;
justify-content: space-between;
padding: 0 10px;
padding: 0 0 0 10px;
width: 100%;
height: auto;
min-height: 40px;
overflow: hidden;
cursor: pointer;
font-size: 0.9rem;
.side-nav-text {
opacity: 1;
min-width: 100px;
word-break: break-all;
-webkit-line-clamp: 1;
text-overflow: ellipsis;
display: -webkit-box !important;
-webkit-box-orient: vertical;
overflow: hidden;
div {
min-width: 102px;
@ -49,7 +56,6 @@
div {
align-items: center;
display: flex;
height: 100%;
justify-content: inherit;
min-width: 30px;

View File

@ -72,10 +72,12 @@ export class SideNavItemComponent implements OnInit {
constructor() {
this.router.events
.pipe(filter(event => event instanceof NavigationEnd),
takeUntilDestroyed(this.destroyRef),
map(evt => evt as NavigationEnd),
tap((evt: NavigationEnd) => this.triggerHighlightCheck(evt.url))
.pipe(
filter(event => event instanceof NavigationEnd),
takeUntilDestroyed(this.destroyRef),
map(evt => evt as NavigationEnd),
tap((evt: NavigationEnd) => this.triggerHighlightCheck(evt.url)),
tap(_ => this.collapseNavIfApplicable())
).subscribe();
}
@ -153,7 +155,6 @@ export class SideNavItemComponent implements OnInit {
// If on mobile, automatically collapse the side nav after making a selection
collapseNavIfApplicable() {
if (this.utilityService.getActiveBreakpoint() < Breakpoint.Tablet) {
console.log('collapsing side nav');
this.navService.collapseSideNav(true);
}
}

View File

@ -73,6 +73,15 @@
}
}
::ng-deep .side-nav-text {
div {
display: flex;
}
span {
font-size: 0.6rem;
}
}
@media (max-width: $grid-breakpoints-lg) {
.side-nav {
padding: 10px 0;

View File

@ -199,8 +199,9 @@ export class PreferenceNavComponent implements AfterViewInit {
))
);
}
if (this.sections[3].children.length === 1) {
this.sections[3].children.push(new SideNavItem(SettingsTabId.MALStackImport, []));
if (this.sections[2].children.length === 1) {
this.sections[2].children.push(new SideNavItem(SettingsTabId.MALStackImport, []));
}
}

View File

@ -27,7 +27,9 @@
<th scope="col">
{{t('platform-label')}}
</th>
<th scope="col"></th>
<th scope="col">
{{t('actions-header')}}
</th>
</tr>
</thead>
<tbody>

View File

@ -1,4 +1,26 @@
@import '../../../theme/variables';
.custom-position {
right: 15px;
top: -42px;
}
.table {
@media (max-width: $grid-breakpoints-sm) {
overflow-x: auto;
width: 100% !important;
display: block;
}
.btn-container {
@media (max-width: $grid-breakpoints-lg) {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
justify-content: center;
}
.btn {
width: 32px;
}
}
}

View File

@ -1,6 +1,6 @@
<ng-container *transloco="let t; read: 'series-detail'">
<app-bulk-operations [actionCallback]="bulkActionCallback" [topOffset]="56"></app-bulk-operations>
<app-bulk-operations [actionCallback]="bulkActionCallback" [topOffset]="55"></app-bulk-operations>
<div [ngStyle]="{'height': ScrollingBlockHeight}" class="main-container container-fluid" #scrollingBlock>
@ -95,7 +95,7 @@
<div class="mt-2">
<div class="row g-0">
<div class="col-6">
<span>{{t('writers-title')}}</span>
<span class="fw-bold">{{t('writers-title')}}</span>
<div>
<app-badge-expander [items]="volumeCast.writers" [allowToggle]="false" (toggle)="switchTabsToDetail()">
<ng-template #badgeExpanderItem let-item let-position="idx" let-last="last">
@ -105,7 +105,7 @@
</div>
</div>
<div class="col-6">
<span>{{t('cover-artists-title')}}</span>
<span class="fw-bold">{{t('cover-artists-title')}}</span>
<div>
<app-badge-expander [items]="volumeCast.coverArtists" [allowToggle]="false" (toggle)="switchTabsToDetail()">
<ng-template #badgeExpanderItem let-item let-position="idx" let-last="last">
@ -155,7 +155,7 @@
<div class="carousel-tabs-container mb-2">
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTabId" class="nav nav-tabs" (navChange)="onNavChange($event)">
<li [ngbNavItem]="TabID.Chapters">
<li [ngbNavItem]="TabID.Chapters">
<a ngbNavLink>
{{utilityService.formatChapterName(libraryType!, false, false, true)}}
<span class="badge rounded-pill text-bg-secondary">{{volume.chapters.length}}</span>
@ -180,28 +180,28 @@
</ng-template>
</li>
@if (volume.chapters.length === 1 && readingLists.length > 0) {
<li [ngbNavItem]="TabID.Related">
<a ngbNavLink>{{t('related-tab')}}</a>
<ng-template ngbNavContent>
@defer (when activeTabId === TabID.Related; prefetch on idle) {
<app-related-tab [readingLists]="readingLists"></app-related-tab>
}
</ng-template>
</li>
}
@if (volume.chapters.length === 1 && readingLists.length > 0) {
<li [ngbNavItem]="TabID.Related">
<a ngbNavLink>{{t('related-tab')}}</a>
<ng-template ngbNavContent>
@defer (when activeTabId === TabID.Related; prefetch on idle) {
<app-related-tab [readingLists]="readingLists"></app-related-tab>
}
</ng-template>
</li>
}
@if (showDetailsTab) {
<li [ngbNavItem]="TabID.Details">
<a ngbNavLink>{{t('details-tab')}}</a>
<ng-template ngbNavContent>
@defer (when activeTabId === TabID.Details; prefetch on idle) {
<app-details-tab [metadata]="volumeCast" [genres]="genres" [tags]="tags"></app-details-tab>
}
</ng-template>
</li>
}
</ul>
@if (showDetailsTab) {
<li [ngbNavItem]="TabID.Details" id="details-tab">
<a ngbNavLink>{{t('details-tab')}}</a>
<ng-template ngbNavContent>
@defer (when activeTabId === TabID.Details; prefetch on idle) {
<app-details-tab [metadata]="volumeCast" [genres]="genres" [tags]="tags"></app-details-tab>
}
</ng-template>
</li>
}
</ul>
</div>
<!-- Min height helps with scroll jerking when switching from chapter -> related/details -->
<div [ngbNavOutlet]="nav" style="min-height: 300px"></div>

View File

@ -646,6 +646,12 @@ export class VolumeDetailComponent implements OnInit {
switchTabsToDetail() {
this.activeTabId = TabID.Details;
this.cdRef.markForCheck();
setTimeout(() => {
const tabElem = this.document.querySelector('#details-tab');
if (tabElem) {
(tabElem as HTMLLIElement).scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
}
}, 10);
}
navigateToSeries() {

View File

@ -7,7 +7,7 @@
// This is responsible for ensuring we scroll down and only tabs and companion bar is visible
.main-container {
// Height set dynamically by get ScrollingBlockHeight()
overflow-y: auto;
overflow: auto;
position: relative;
overscroll-behavior-y: none;
scrollbar-gutter: stable;

View File

@ -250,13 +250,15 @@
"add": "{{common.add}}",
"delete": "{{common.delete}}",
"edit": "{{common.edit}}",
"no-data": "{{typeahead.no-data}}"
"no-data": "{{typeahead.no-data}}",
"actions-header": "{{manage-users.actions-header}}"
},
"edit-device-modal": {
"title": "Edit Device",
"device-name-label": "{{manage-devices.name-label}}",
"platform-label": "{{manage-devices.platform-label}}",
"email-label": "{{common.email}}",
"email-tooltip": "This email will be used to accept the file via Send To",
"device-platform-label": "Device Platform",
@ -672,7 +674,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": "Invalidate an previous registration using your license. Requires both License and Email",
"activate-reset-tooltip": "Invalidate a 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 ",

View File

@ -101,15 +101,17 @@ app-root {
}
body {
scrollbar-gutter: stable both-edges;
font-family: 'Poppins', sans-serif;
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;
padding: 0;
margin: 0;
@media (max-width: $grid-breakpoints-lg) {
margin-top: var(--nav-mobile-offset) !important;
height: calc(var(--vh)* 100 - var(--nav-mobile-offset)) !important;
/* Setting this break the readers */
//margin-top: var(--nav-mobile-offset) !important;
//height: calc(var(--vh)* 100 - var(--nav-mobile-offset)) !important;
}
}
@ -118,4 +120,4 @@ body {
height: 1px;
background-color: var(--setting-break-color);
margin: 30px 0;
}
}

View File

@ -1,88 +1,19 @@
@import '../variables';
.sidenav-bottom {
position: absolute;
bottom: 0;
width: 190px;
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;
overflow-y: auto;
left: -50px;
}
}
:host ::ng-deep .sidenav-bottom .donate .side-nav-item {
justify-content: center;
min-height: 25px;
align-items: center;
:hover {
background-color: unset;
}
}
:host ::ng-deep .sidenav-bottom .donate .side-nav-item span {
flex-grow: unset !important;
min-width: unset !important;;
}
:host ::ng-deep .sidenav-bottom .donate .side-nav-item span div {
min-width: unset !important;
}
:host ::ng-deep .sidenav-bottom .donate .side-nav-item span div i{
font-size: 12px !important;
}
:host ::ng-deep .sidenav-bottom .donate .side-nav-item.closed span.phone-hidden div {
width: 100%;
}
:host ::ng-deep .sidenav-bottom .donate .side-nav-item.closed span.side-nav-text div {
width: 0;
}
@media (max-width: $grid-breakpoints-lg) {
:host ::ng-deep .sidenav-bottom .donate .side-nav-item.closed {
display: none;
}
:host ::ng-deep .sidenav-bottom .donate .side-nav-item span.phone-hidden {
display: block !important;
}
}
@import "../variables";
.side-nav-container {
padding-bottom: 10px;
width: 190px;
background-color: var(--side-nav-bg-color);
height: calc((var(--vh)*100) - 115px);
height: calc((var(--vh) * 100) - 115px);
position: fixed;
margin: 0;
left: 10px;
top: 73px;
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);
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 {
@ -90,11 +21,11 @@
}
&.preference {
height: calc((var(--vh)*100));
height: calc((var(--vh) * 100));
}
&.no-donate {
height: calc((var(--vh)*100) - 82px);
height: calc((var(--vh) * 100) - 82px);
}
&.hidden {
@ -117,39 +48,39 @@
-webkit-mask-image: linear-gradient(to bottom, transparent, black 0%, black 97%, transparent 100%);
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);
@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 {
visibility: visible;
background-color: rgba(255,255,255,0.3); /*On hover, it will turn grey*/
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*/
}
}
}
.side-nav-item:first {
border-top-left-radius: var(--side-nav-border-radius);
@ -158,6 +89,85 @@
}
}
.sidenav-bottom {
position: absolute;
bottom: 0;
width: 190px;
font-size: 12px;
transition: width var(--side-nav-openclose-transition);
z-index: 999;
left: 10px;
.donate {
.side-nav-item {
width: 100%;
padding: 0 80px;
justify-content: center;
align-items: center;
&:hover {
background-color: unset !important;
color: white !important;
i {
color: var(--side-nav-item-closed-color) !important;
&:hover {
color: white !important;
}
}
}
span {
flex-grow: unset !important;
min-width: unset !important;
div {
min-width: unset !important;
i {
font-size: 1rem !important;
}
}
}
&.closed {
span {
&.phone-hidden {
div {
width: 100%;
}
}
.side-nav-text {
div {
width: 0;
}
}
}
}
}
}
&.closed {
width: 45px;
overflow-x: hidden;
overflow-y: auto;
.side-nav-item {
width: 100%;
padding: 0;
display: block;
line-height: 40px;
text-align: center;
&:hover {
background-color: unset;
}
}
}
}
@media (max-width: $grid-breakpoints-lg) {
.side-nav-container {
padding: 10px 0;
@ -170,11 +180,9 @@
top: 0;
transition: width var(--side-nav-openclose-transition);
z-index: 1050;
overflow: auto;
overflow-y: auto;
border: var(--side-nav-mobile-border);
&.no-donate {
height: 100dvh;
}
@ -185,6 +193,14 @@
box-shadow: none;
}
.side-nav {
overflow: auto;
}
.side-nav-item {
padding: 0;
}
.side-nav-item:first {
border-top-left-radius: var(--side-nav-border-radius);
border-top-right-radius: var(--side-nav-border-radius);
@ -192,14 +208,16 @@
}
.sidenav-bottom {
display:none;
display: none;
&.closed {
left: 10px;
}
}
.side-nav-overlay {
background-color: var(--side-nav-overlay-color);
width: 100vw;
height: calc((var(--vh)*100) - var(--nav-mobile-offset));
height: calc((var(--vh) * 100) - var(--nav-mobile-offset));
position: absolute;
left: 0;
top: var(--nav-mobile-offset);

View File

@ -53,8 +53,6 @@
--default-state-scrollbar: transparent;
--text-muted-color: hsla(0,0%,100%,.45);
/* New Color scheme */
--bulk-background-color: rgba(39,39,39,1);
/* Theming colors that performs a gradient for background. Can be disabled else automatically applied based on cover image colors.
* --colorscape-primary-color and the alpha variants will be updated in real time. the default variant is fixed and represents the default state and should
@ -82,14 +80,15 @@
--theme-color: #000000;
--color-scheme: dark;
--tile-color: var(--primary-color);
--nav-offset: 70px;
--nav-offset: 60px;
--nav-mobile-offset: 55px;
/* Should we render the series cover as background on mobile */
--mobile-series-img-background: true;
/* Setting Item */
// TODO: Robbie let's refactor this so all setting classes inherit from this area
--h6-text-color: #d5d5d5;
--h6-font-size: 1.2rem;
--h6-font-weight: bold;
--setting-header-text-color: #d5d5d5;
--setting-header-font-size: 1.2rem;
--setting-header-font-weight: bold;
--setting-break-color: rgba(255, 255, 255, 0.2);
@ -100,6 +99,7 @@
--table-body-text-color: hsla(0,0%,100%,.85);
--table-body-striped-bg-color: hsla(0,0%,100%,.25);
--table-body-border: hidden;
--table-body-striped-bg-color: var(--elevation-layer2);
/* Navbar */
@ -383,6 +383,7 @@
/* Bulk Selection */
--bulk-selection-text-color: var(--navbar-text-color);
--bulk-selection-highlight-text-color: var(--primary-color);
--bulk-selection-bg-color: rgba(39,39,39,1);
/* List Card Item */
--card-list-item-bg-color: linear-gradient(180deg, rgba(0,0,0,0.15) 0%, rgba(0,0,0,0.15) 1%, rgba(0,0,0,0) 100%);
@ -410,7 +411,6 @@
--login-input-box-shadow-focus: 0 0 0 1px rgba(74, 198, 148, 0.8);
--login-input-background-color: #353535;
--login-input-color: #fff;
--login-input-placeholder-color: #cecece;
--login-forgot-password-color: var(--primary-color);
--login-background-url: url('../../assets/images/login-bg.jpg');
--login-background-size: cover;
@ -419,6 +419,4 @@
--login-input-font-family: 'League Spartan', sans-serif;
--login-input-placeholder-opacity: 0.5;
--login-input-placeholder-color: #fff;
--mobile-series-img-background: true;
}

View File

@ -1,5 +1,5 @@
h6.section-title {
color: var(--h6-text-color);
font-weight: var(--h6-font-weight);
font-size: var(--h6-font-size);
color: var(--setting-header-text-color);
font-weight: var(--setting-header-font-weight);
font-size: var(--setting-header-font-size);
}