mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-24 00:52:23 -04:00
UX Pass 7 (#3135)
Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
This commit is contained in:
parent
5bf5558212
commit
79eb98a3bb
8
.github/workflows/codeql.yml
vendored
8
.github/workflows/codeql.yml
vendored
@ -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}}"
|
||||
|
@ -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" />
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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" />
|
||||
|
@ -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
|
||||
|
@ -1 +0,0 @@
|
||||
{}
|
@ -1 +0,0 @@
|
||||
{}
|
@ -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.
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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){
|
||||
|
@ -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 ? {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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}}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -0,0 +1,4 @@
|
||||
.custom-position {
|
||||
right: 5px;
|
||||
top: -42px;
|
||||
}
|
@ -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>
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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}}
|
||||
@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}}
|
||||
@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>
|
||||
|
@ -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>
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -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">
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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>
|
||||
|
@ -0,0 +1,3 @@
|
||||
.main-container {
|
||||
margin-top: 10px;
|
||||
}
|
@ -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>
|
@ -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 {
|
||||
|
||||
|
@ -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>
|
||||
|
||||
|
@ -0,0 +1,4 @@
|
||||
.main-container {
|
||||
margin-top: 10px;
|
||||
padding: 0 0 0 10px;
|
||||
}
|
@ -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";
|
||||
|
||||
|
@ -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>
|
||||
|
@ -8,3 +8,8 @@
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
|
||||
.main-container {
|
||||
margin-top: 10px;
|
||||
}
|
@ -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>
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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"> {{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"> {{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"> {{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"> {{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"> {{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"> {{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>
|
||||
|
@ -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>
|
||||
|
@ -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 + ''));
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)"
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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, []));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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() {
|
||||
|
@ -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;
|
||||
|
@ -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 ",
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user