mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Polish Part 3 (#2424)
This commit is contained in:
parent
a018d6828e
commit
944830ca73
4
.github/workflows/build-and-test.yml
vendored
4
.github/workflows/build-and-test.yml
vendored
@ -65,9 +65,9 @@ jobs:
|
|||||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||||
shell: powershell
|
shell: powershell
|
||||||
run: |
|
run: |
|
||||||
.\.sonar\scanner\dotnet-sonarscanner begin /k:"Kareadita_Kavita" /o:"kareadita" /d:sonar.login="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io"
|
.\.sonar\scanner\dotnet-sonarscanner begin /k:"Kareadita_Kavita" /o:"kareadita" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io"
|
||||||
dotnet build --configuration Release
|
dotnet build --configuration Release
|
||||||
.\.sonar\scanner\dotnet-sonarscanner end /d:sonar.login="${{ secrets.SONAR_TOKEN }}"
|
.\.sonar\scanner\dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}"
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: dotnet test --no-restore --verbosity normal
|
run: dotnet test --no-restore --verbosity normal
|
||||||
|
@ -7,11 +7,11 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.13" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.13" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||||
<PackageReference Include="NSubstitute" Version="5.1.0" />
|
<PackageReference Include="NSubstitute" Version="5.1.0" />
|
||||||
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="19.2.69" />
|
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="19.2.69" />
|
||||||
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="19.2.69" />
|
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="19.2.69" />
|
||||||
<PackageReference Include="xunit" Version="2.6.0" />
|
<PackageReference Include="xunit" Version="2.6.1" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3">
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
@ -12,6 +12,18 @@ namespace API.Tests.Helpers;
|
|||||||
|
|
||||||
public class SmartFilterHelperTests
|
public class SmartFilterHelperTests
|
||||||
{
|
{
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("", false)]
|
||||||
|
[InlineData("name=DC%20-%20On%20Deck&stmts=comparison%3D1%26field%3D20%26value%3D0,comparison%3D9%26field%3D20%26value%3D100,comparison%3D0%26field%3D19%26value%3D274&sortOptions=sortField%3D1&isAscending=True&limitTo=0&combination=1", true)]
|
||||||
|
[InlineData("name=English%20In%20Progress&stmts=comparison%253D8%252Cfield%253D7%252Cvalue%253D4%25252C3,comparison%253D3%252Cfield%253D20%252Cvalue%253D100,comparison%253D8%252Cfield%253D3%252Cvalue%253Dja,comparison%253D1%252Cfield%253D20%252Cvalue%253D0&sortOptions=sortField%3D7,isAscending%3DFalse&limitTo=0&combination=1", true)]
|
||||||
|
[InlineData("name=Unread%20Isekai%20Light%20Novels&stmts=comparison%253D0%25C2%25A6field%253D20%25C2%25A6value%253D0%EF%BF%BDcomparison%253D5%25C2%25A6field%253D6%25C2%25A6value%253D230%EF%BF%BDcomparison%253D8%25C2%25A6field%253D7%25C2%25A6value%253D4%EF%BF%BDcomparison%253D0%25C2%25A6field%253D19%25C2%25A6value%253D14&sortOptions=sortField%3D5%C2%A6isAscending%3DFalse&limitTo=0&combination=1", false)]
|
||||||
|
[InlineData("name=Zero&stmts=comparison%3d7%26field%3d1%26value%3d0&sortOptions=sortField=2&isAscending=False&limitTo=0&combination=1", true)]
|
||||||
|
public void Test_ShouldMigrateFilter(string filter, bool expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, MigrateSmartFilterEncoding.ShouldMigrateFilter(filter));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Test_Decode()
|
public void Test_Decode()
|
||||||
{
|
{
|
||||||
|
@ -94,13 +94,13 @@
|
|||||||
<PackageReference Include="NetVips" Version="2.3.1" />
|
<PackageReference Include="NetVips" Version="2.3.1" />
|
||||||
<PackageReference Include="NetVips.Native" Version="8.14.5" />
|
<PackageReference Include="NetVips.Native" Version="8.14.5" />
|
||||||
<PackageReference Include="NReco.Logging.File" Version="1.1.7" />
|
<PackageReference Include="NReco.Logging.File" Version="1.1.7" />
|
||||||
<PackageReference Include="Serilog" Version="3.0.1" />
|
<PackageReference Include="Serilog" Version="3.1.0" />
|
||||||
<PackageReference Include="Serilog.AspNetCore" Version="7.0.0" />
|
<PackageReference Include="Serilog.AspNetCore" Version="7.0.0" />
|
||||||
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.2.0-dev-00752" />
|
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.2.0-dev-00752" />
|
||||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="7.0.0" />
|
<PackageReference Include="Serilog.Extensions.Hosting" Version="7.0.0" />
|
||||||
<PackageReference Include="Serilog.Settings.Configuration" Version="7.0.1" />
|
<PackageReference Include="Serilog.Settings.Configuration" Version="7.0.1" />
|
||||||
<PackageReference Include="Serilog.Sinks.AspNetCore.SignalR" Version="0.4.0" />
|
<PackageReference Include="Serilog.Sinks.AspNetCore.SignalR" Version="0.4.0" />
|
||||||
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
|
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.0" />
|
||||||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||||
<PackageReference Include="Serilog.Sinks.SignalR.Core" Version="0.1.2" />
|
<PackageReference Include="Serilog.Sinks.SignalR.Core" Version="0.1.2" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.34.1" />
|
<PackageReference Include="SharpCompress" Version="0.34.1" />
|
||||||
|
@ -27,7 +27,7 @@ public static class MigrateSmartFilterEncoding
|
|||||||
var smartFilters = dataContext.AppUserSmartFilter.ToList();
|
var smartFilters = dataContext.AppUserSmartFilter.ToList();
|
||||||
foreach (var filter in smartFilters)
|
foreach (var filter in smartFilters)
|
||||||
{
|
{
|
||||||
if (filter.Filter.Contains(SmartFilterHelper.StatementSeparator)) continue;
|
if (!ShouldMigrateFilter(filter.Filter)) continue;
|
||||||
var decode = EncodeFix(filter.Filter);
|
var decode = EncodeFix(filter.Filter);
|
||||||
if (string.IsNullOrEmpty(decode)) continue;
|
if (string.IsNullOrEmpty(decode)) continue;
|
||||||
filter.Filter = decode;
|
filter.Filter = decode;
|
||||||
@ -41,6 +41,11 @@ public static class MigrateSmartFilterEncoding
|
|||||||
logger.LogCritical("Running MigrateSmartFilterEncoding migration - Completed. This is not an error");
|
logger.LogCritical("Running MigrateSmartFilterEncoding migration - Completed. This is not an error");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool ShouldMigrateFilter(string filter)
|
||||||
|
{
|
||||||
|
return !string.IsNullOrEmpty(filter) && !(filter.Contains(SmartFilterHelper.StatementSeparator) || Uri.UnescapeDataString(filter).Contains(SmartFilterHelper.StatementSeparator));
|
||||||
|
}
|
||||||
|
|
||||||
public static string EncodeFix(string encodedFilter)
|
public static string EncodeFix(string encodedFilter)
|
||||||
{
|
{
|
||||||
var statements = StatementsRegex.Matches(encodedFilter)
|
var statements = StatementsRegex.Matches(encodedFilter)
|
||||||
@ -67,6 +72,7 @@ public static class MigrateSmartFilterEncoding
|
|||||||
return $"sortField={sortFieldValue}{SmartFilterHelper.InnerStatementSeparator}isAscending={isAscendingValue}";
|
return $"sortField={sortFieldValue}{SmartFilterHelper.InnerStatementSeparator}isAscending={isAscendingValue}";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//name=Zero&sortOptions=sortField=2&isAscending=False&limitTo=0&combination=1
|
||||||
var filterDto = SmartFilterHelper.Decode(noStmt);
|
var filterDto = SmartFilterHelper.Decode(noStmt);
|
||||||
|
|
||||||
// Now we just parse each individual stmt into the core components and add to statements
|
// Now we just parse each individual stmt into the core components and add to statements
|
||||||
|
@ -189,6 +189,10 @@
|
|||||||
"user-no-access-library-from-series": "User does not have access to the library this series belongs to",
|
"user-no-access-library-from-series": "User does not have access to the library this series belongs to",
|
||||||
"series-restricted-age-restriction": "User is not allowed to view this series due to age restrictions",
|
"series-restricted-age-restriction": "User is not allowed to view this series due to age restrictions",
|
||||||
|
|
||||||
|
"next-volume-num": "Next Volume: {0}",
|
||||||
|
"next-book-num": "Next Book: {0}",
|
||||||
|
"next-issue-num": "Next Issue: {0}{1}",
|
||||||
|
"next-chapter-num": "Next Chapter: {0}",
|
||||||
|
|
||||||
|
|
||||||
"volume-num": "Volume {0}",
|
"volume-num": "Volume {0}",
|
||||||
|
@ -745,13 +745,14 @@ public class SeriesService : ISeriesService
|
|||||||
result.VolumeNumber = lastChapter.Volume.Number;
|
result.VolumeNumber = lastChapter.Volume.Number;
|
||||||
result.Title = series.Library.Type switch
|
result.Title = series.Library.Type switch
|
||||||
{
|
{
|
||||||
LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num",
|
LibraryType.Manga => await _localizationService.Translate(userId, "next-chapter-num",
|
||||||
new object[] {result.ChapterNumber}),
|
new object[] {result.ChapterNumber}),
|
||||||
LibraryType.Comic => await _localizationService.Translate(userId, "issue-num",
|
LibraryType.Comic => await _localizationService.Translate(userId, "next-issue-num",
|
||||||
new object[] {"#", result.ChapterNumber}),
|
new object[] {"#", result.ChapterNumber}),
|
||||||
LibraryType.Book => await _localizationService.Translate(userId, "book-num",
|
LibraryType.Book => await _localizationService.Translate(userId, "next-book-num",
|
||||||
new object[] {result.ChapterNumber}),
|
new object[] {result.ChapterNumber}),
|
||||||
_ => "Chapter " + result.ChapterNumber
|
_ => await _localizationService.Translate(userId, "next-chapter-num",
|
||||||
|
new object[] {result.ChapterNumber})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -294,7 +294,8 @@ public class ProcessSeries : IProcessSeries
|
|||||||
var maxVolume = series.Volumes.Max(v => (int) Parser.Parser.MaxNumberFromRange(v.Name));
|
var maxVolume = series.Volumes.Max(v => (int) Parser.Parser.MaxNumberFromRange(v.Name));
|
||||||
var maxChapter = chapters.Max(c => (int) Parser.Parser.MaxNumberFromRange(c.Range));
|
var maxChapter = chapters.Max(c => (int) Parser.Parser.MaxNumberFromRange(c.Range));
|
||||||
|
|
||||||
if (maxChapter > series.Metadata.TotalCount && maxVolume <= series.Metadata.TotalCount)
|
|
||||||
|
if ((maxChapter == 0 || maxChapter > series.Metadata.TotalCount) && maxVolume <= series.Metadata.TotalCount)
|
||||||
{
|
{
|
||||||
series.Metadata.MaxCount = maxVolume;
|
series.Metadata.MaxCount = maxVolume;
|
||||||
}
|
}
|
||||||
|
@ -225,13 +225,13 @@ public class Startup
|
|||||||
IDirectoryService directoryService, IUnitOfWork unitOfWork, IBackupService backupService, IImageService imageService)
|
IDirectoryService directoryService, IUnitOfWork unitOfWork, IBackupService backupService, IImageService imageService)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
var logger = serviceProvider.GetRequiredService<ILogger<Program>>();
|
||||||
// Apply Migrations
|
// Apply Migrations
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Task.Run(async () =>
|
Task.Run(async () =>
|
||||||
{
|
{
|
||||||
// Apply all migrations on startup
|
// Apply all migrations on startup
|
||||||
var logger = serviceProvider.GetRequiredService<ILogger<Program>>();
|
|
||||||
var userManager = serviceProvider.GetRequiredService<UserManager<AppUser>>();
|
var userManager = serviceProvider.GetRequiredService<UserManager<AppUser>>();
|
||||||
var dataContext = serviceProvider.GetRequiredService<DataContext>();
|
var dataContext = serviceProvider.GetRequiredService<DataContext>();
|
||||||
|
|
||||||
@ -256,7 +256,6 @@ public class Startup
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
var logger = serviceProvider.GetRequiredService<ILogger<Program>>();
|
|
||||||
logger.LogCritical(ex, "An error occurred during migration");
|
logger.LogCritical(ex, "An error occurred during migration");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -377,7 +376,6 @@ public class Startup
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var logger = serviceProvider.GetRequiredService<ILogger<Startup>>();
|
|
||||||
logger.LogInformation("Kavita - v{Version}", BuildInfo.Version);
|
logger.LogInformation("Kavita - v{Version}", BuildInfo.Version);
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
@ -387,8 +385,7 @@ public class Startup
|
|||||||
Console.WriteLine($"Kavita - v{BuildInfo.Version}");
|
Console.WriteLine($"Kavita - v{BuildInfo.Version}");
|
||||||
});
|
});
|
||||||
|
|
||||||
var _logger = serviceProvider.GetRequiredService<ILogger<Startup>>();
|
logger.LogInformation("Starting with base url as {BaseUrl}", basePath);
|
||||||
_logger.LogInformation("Starting with base url as {BaseUrl}", basePath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void UpdateBaseUrlInIndex(string baseUrl)
|
private static void UpdateBaseUrlInIndex(string baseUrl)
|
||||||
|
BIN
API/config/config.7z
Normal file
BIN
API/config/config.7z
Normal file
Binary file not shown.
@ -1,22 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Flurl.Http" Version="3.2.0" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\Kavita.Common\Kavita.Common.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="DTOs" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
@ -77,7 +77,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "anyComponentStyle",
|
"type": "anyComponentStyle",
|
||||||
"maximumWarning": "2kb",
|
"maximumWarning": "4kb",
|
||||||
"maximumError": "30kb"
|
"maximumError": "30kb"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
13
UI/Web/package-lock.json
generated
13
UI/Web/package-lock.json
generated
@ -33,6 +33,7 @@
|
|||||||
"@tweenjs/tween.js": "^21.0.0",
|
"@tweenjs/tween.js": "^21.0.0",
|
||||||
"bootstrap": "^5.3.2",
|
"bootstrap": "^5.3.2",
|
||||||
"charts.css": "^1.1.0",
|
"charts.css": "^1.1.0",
|
||||||
|
"file-saver": "^2.0.5",
|
||||||
"luxon": "^3.4.3",
|
"luxon": "^3.4.3",
|
||||||
"ng-circle-progress": "^1.7.1",
|
"ng-circle-progress": "^1.7.1",
|
||||||
"ng-lazyload-image": "^9.1.3",
|
"ng-lazyload-image": "^9.1.3",
|
||||||
@ -59,6 +60,7 @@
|
|||||||
"@angular/cli": "^17.0.0",
|
"@angular/cli": "^17.0.0",
|
||||||
"@angular/compiler-cli": "^17.0.1",
|
"@angular/compiler-cli": "^17.0.1",
|
||||||
"@types/d3": "^7.4.3",
|
"@types/d3": "^7.4.3",
|
||||||
|
"@types/file-saver": "^2.0.7",
|
||||||
"@types/luxon": "^3.3.4",
|
"@types/luxon": "^3.3.4",
|
||||||
"@types/node": "^20.9.0",
|
"@types/node": "^20.9.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
||||||
@ -4710,6 +4712,12 @@
|
|||||||
"@types/send": "*"
|
"@types/send": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/file-saver": {
|
||||||
|
"version": "2.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz",
|
||||||
|
"integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/geojson": {
|
"node_modules/@types/geojson": {
|
||||||
"version": "7946.0.10",
|
"version": "7946.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz",
|
||||||
@ -8719,6 +8727,11 @@
|
|||||||
"node": "^10.12.0 || >=12.0.0"
|
"node": "^10.12.0 || >=12.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/file-saver": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
|
||||||
|
},
|
||||||
"node_modules/filelist": {
|
"node_modules/filelist": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
|
||||||
|
@ -38,6 +38,7 @@
|
|||||||
"@tweenjs/tween.js": "^21.0.0",
|
"@tweenjs/tween.js": "^21.0.0",
|
||||||
"bootstrap": "^5.3.2",
|
"bootstrap": "^5.3.2",
|
||||||
"charts.css": "^1.1.0",
|
"charts.css": "^1.1.0",
|
||||||
|
"file-saver": "^2.0.5",
|
||||||
"luxon": "^3.4.3",
|
"luxon": "^3.4.3",
|
||||||
"ng-circle-progress": "^1.7.1",
|
"ng-circle-progress": "^1.7.1",
|
||||||
"ng-lazyload-image": "^9.1.3",
|
"ng-lazyload-image": "^9.1.3",
|
||||||
@ -64,6 +65,7 @@
|
|||||||
"@angular/cli": "^17.0.0",
|
"@angular/cli": "^17.0.0",
|
||||||
"@angular/compiler-cli": "^17.0.1",
|
"@angular/compiler-cli": "^17.0.1",
|
||||||
"@types/d3": "^7.4.3",
|
"@types/d3": "^7.4.3",
|
||||||
|
"@types/file-saver": "^2.0.7",
|
||||||
"@types/luxon": "^3.3.4",
|
"@types/luxon": "^3.3.4",
|
||||||
"@types/node": "^20.9.0",
|
"@types/node": "^20.9.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
||||||
|
@ -11,7 +11,7 @@ export class SafeHtmlPipe implements PipeTransform {
|
|||||||
private readonly dom: DomSanitizer = inject(DomSanitizer);
|
private readonly dom: DomSanitizer = inject(DomSanitizer);
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
transform(value: string): unknown {
|
transform(value: string): string | null {
|
||||||
return this.dom.sanitize(SecurityContext.HTML, value);
|
return this.dom.sanitize(SecurityContext.HTML, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
10
UI/Web/src/app/_providers/saver.provider.ts
Normal file
10
UI/Web/src/app/_providers/saver.provider.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import {InjectionToken} from '@angular/core'
|
||||||
|
import { saveAs } from 'file-saver';
|
||||||
|
|
||||||
|
export type Saver = (blob: Blob, filename?: string) => void
|
||||||
|
|
||||||
|
export const SAVER = new InjectionToken<Saver>('saver')
|
||||||
|
|
||||||
|
export function getSaver(): Saver {
|
||||||
|
return saveAs;
|
||||||
|
}
|
7
UI/Web/src/app/_routes/all-filters-routing.module.ts
Normal file
7
UI/Web/src/app/_routes/all-filters-routing.module.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import {Routes} from "@angular/router";
|
||||||
|
import {AllFiltersComponent} from "../all-filters/all-filters.component";
|
||||||
|
|
||||||
|
|
||||||
|
export const routes: Routes = [
|
||||||
|
{path: '', component: AllFiltersComponent, pathMatch: 'full'},
|
||||||
|
];
|
@ -1,14 +1,7 @@
|
|||||||
import { Routes } from "@angular/router";
|
import { Routes } from "@angular/router";
|
||||||
import { AuthGuard } from "../_guards/auth.guard";
|
|
||||||
import { AllSeriesComponent } from "../all-series/_components/all-series/all-series.component";
|
import { AllSeriesComponent } from "../all-series/_components/all-series/all-series.component";
|
||||||
|
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{path: '**', component: AllSeriesComponent, pathMatch: 'full', canActivate: [AuthGuard]},
|
{path: '', component: AllSeriesComponent, pathMatch: 'full'},
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
component: AllSeriesComponent,
|
|
||||||
runGuardsAndResolvers: 'always',
|
|
||||||
canActivate: [AuthGuard],
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
|
@ -1,16 +1,6 @@
|
|||||||
import { Routes } from "@angular/router";
|
import { Routes } from "@angular/router";
|
||||||
import { AdminGuard } from "../_guards/admin.guard";
|
|
||||||
import { AuthGuard } from "../_guards/auth.guard";
|
|
||||||
import { AnnouncementsComponent } from "../announcements/_components/announcements/announcements.component";
|
import { AnnouncementsComponent } from "../announcements/_components/announcements/announcements.component";
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{path: '**', component: AnnouncementsComponent, pathMatch: 'full', canActivate: [AuthGuard, AdminGuard]},
|
{path: '', component: AnnouncementsComponent, pathMatch: 'full'},
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
runGuardsAndResolvers: 'always',
|
|
||||||
canActivate: [AuthGuard, AdminGuard],
|
|
||||||
children: [
|
|
||||||
{path: 'announcements', component: AnnouncementsComponent},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
|
@ -1,15 +1,6 @@
|
|||||||
import { Routes } from "@angular/router";
|
import { Routes } from "@angular/router";
|
||||||
import { AuthGuard } from "../_guards/auth.guard";
|
|
||||||
import { BookmarksComponent } from "../bookmark/_components/bookmarks/bookmarks.component";
|
import { BookmarksComponent } from "../bookmark/_components/bookmarks/bookmarks.component";
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{path: '**', component: BookmarksComponent, pathMatch: 'full', canActivate: [AuthGuard]},
|
{path: '', component: BookmarksComponent, pathMatch: 'full'},
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
runGuardsAndResolvers: 'always',
|
|
||||||
canActivate: [AuthGuard],
|
|
||||||
children: [
|
|
||||||
{path: 'bookmarks', component: BookmarksComponent},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
|
@ -1,17 +1,9 @@
|
|||||||
import { Routes } from '@angular/router';
|
import { Routes } from '@angular/router';
|
||||||
import { AuthGuard } from '../_guards/auth.guard';
|
|
||||||
import { AllCollectionsComponent } from '../collections/_components/all-collections/all-collections.component';
|
import { AllCollectionsComponent } from '../collections/_components/all-collections/all-collections.component';
|
||||||
import { CollectionDetailComponent } from '../collections/_components/collection-detail/collection-detail.component';
|
import { CollectionDetailComponent } from '../collections/_components/collection-detail/collection-detail.component';
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{
|
{path: '', component: AllCollectionsComponent, pathMatch: 'full'},
|
||||||
path: '',
|
{path: ':id', component: CollectionDetailComponent},
|
||||||
runGuardsAndResolvers: 'always',
|
|
||||||
canActivate: [AuthGuard],
|
|
||||||
children: [
|
|
||||||
{path: '', component: AllCollectionsComponent, pathMatch: 'full'},
|
|
||||||
{path: ':id', component: CollectionDetailComponent},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
import { Routes } from '@angular/router';
|
import { Routes } from '@angular/router';
|
||||||
import { AuthGuard } from '../_guards/auth.guard';
|
|
||||||
import { DashboardComponent } from '../dashboard/_components/dashboard.component';
|
import { DashboardComponent } from '../dashboard/_components/dashboard.component';
|
||||||
|
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
runGuardsAndResolvers: 'always',
|
|
||||||
canActivate: [AuthGuard],
|
|
||||||
component: DashboardComponent,
|
component: DashboardComponent,
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
@ -1,18 +1,9 @@
|
|||||||
import { Routes } from "@angular/router";
|
import { Routes } from "@angular/router";
|
||||||
import { AuthGuard } from "../_guards/auth.guard";
|
|
||||||
import { ReadingListDetailComponent } from "../reading-list/_components/reading-list-detail/reading-list-detail.component";
|
import { ReadingListDetailComponent } from "../reading-list/_components/reading-list-detail/reading-list-detail.component";
|
||||||
import { ReadingListsComponent } from "../reading-list/_components/reading-lists/reading-lists.component";
|
import { ReadingListsComponent } from "../reading-list/_components/reading-lists/reading-lists.component";
|
||||||
|
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{
|
{path: '', component: ReadingListsComponent, pathMatch: 'full'},
|
||||||
path: '',
|
{path: ':id', component: ReadingListDetailComponent, pathMatch: 'full'},
|
||||||
runGuardsAndResolvers: 'always',
|
|
||||||
canActivate: [AuthGuard],
|
|
||||||
children: [
|
|
||||||
{path: '', component: ReadingListsComponent, pathMatch: 'full'},
|
|
||||||
{path: ':id', component: ReadingListDetailComponent, pathMatch: 'full'},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{path: '**', component: ReadingListsComponent, pathMatch: 'full', canActivate: [AuthGuard]},
|
|
||||||
];
|
];
|
||||||
|
@ -1,15 +1,6 @@
|
|||||||
import { Routes } from '@angular/router';
|
import { Routes } from '@angular/router';
|
||||||
import { AuthGuard } from '../_guards/auth.guard';
|
|
||||||
import { UserPreferencesComponent } from '../user-settings/user-preferences/user-preferences.component';
|
import { UserPreferencesComponent } from '../user-settings/user-preferences/user-preferences.component';
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{path: '**', component: UserPreferencesComponent, pathMatch: 'full'},
|
{path: '', component: UserPreferencesComponent, pathMatch: 'full'},
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
runGuardsAndResolvers: 'always',
|
|
||||||
canActivate: [AuthGuard],
|
|
||||||
children: [
|
|
||||||
{path: '', component: UserPreferencesComponent, pathMatch: 'full'},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
|
@ -1,15 +1,6 @@
|
|||||||
import { Routes } from '@angular/router';
|
import { Routes } from '@angular/router';
|
||||||
import { AuthGuard } from '../_guards/auth.guard';
|
|
||||||
import { WantToReadComponent } from '../want-to-read/_components/want-to-read/want-to-read.component';
|
import { WantToReadComponent } from '../want-to-read/_components/want-to-read/want-to-read.component';
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{path: '**', component: WantToReadComponent, pathMatch: 'full'},
|
{path: '', component: WantToReadComponent, pathMatch: 'full'},
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
runGuardsAndResolvers: 'always',
|
|
||||||
canActivate: [AuthGuard],
|
|
||||||
children: [
|
|
||||||
{path: '', component: WantToReadComponent, pathMatch: 'full'},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<ng-container *transloco="let t; read:'review-card'">
|
<ng-container *transloco="let t; read:'review-card'">
|
||||||
<div class="card mb-3" style="max-width: 320px; max-height: 160px; height: 160px" (click)="showModal()">
|
<div class="card clickable mb-3" style="max-width: 320px; max-height: 160px; height: 160px" (click)="showModal()">
|
||||||
<div class="row g-0">
|
<div class="row g-0">
|
||||||
<div class="col-md-2 d-none d-md-block">
|
<div class="col-md-2 d-none d-md-block">
|
||||||
<i class="img-fluid rounded-start fa-solid fa-circle-user profile-image" aria-hidden="true"></i>
|
<i class="img-fluid rounded-start fa-solid fa-circle-user profile-image" aria-hidden="true"></i>
|
||||||
|
@ -32,10 +32,6 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-images img {
|
.no-images img {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,5 @@
|
|||||||
background-color: var(--review-spoiler-bg-color);
|
background-color: var(--review-spoiler-bg-color);
|
||||||
color: var(--review-spoiler-text-color);
|
color: var(--review-spoiler-text-color);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@
|
|||||||
<td><i class="fa-solid fa-arrow-turn-up" aria-hidden="true"></i></td>
|
<td><i class="fa-solid fa-arrow-turn-up" aria-hidden="true"></i></td>
|
||||||
<td>...</td>
|
<td>...</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr *ngFor="let folder of folders; let idx = index;" (click)="selectNode(folder)" style="cursor: pointer;" [ngClass]="{'disabled': folder.disabled}">
|
<tr *ngFor="let folder of folders; let idx = index;" (click)="selectNode(folder)" class="clickable" [ngClass]="{'disabled': folder.disabled}">
|
||||||
<td><i class="fa-regular fa-folder" aria-hidden="true"></i></td>
|
<td><i class="fa-regular fa-folder" aria-hidden="true"></i></td>
|
||||||
<td id="folder--{{idx}}">
|
<td id="folder--{{idx}}">
|
||||||
{{folder.name}}
|
{{folder.name}}
|
||||||
|
34
UI/Web/src/app/all-filters/all-filters.component.html
Normal file
34
UI/Web/src/app/all-filters/all-filters.component.html
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<ng-container *transloco="let t; read: 'all-filters'">
|
||||||
|
<app-side-nav-companion-bar [hasFilter]="false">
|
||||||
|
<h2 title>
|
||||||
|
{{t('title')}}
|
||||||
|
</h2>
|
||||||
|
<h6 subtitle >{{t('count', {count: filters.length | number})}}</h6>
|
||||||
|
</app-side-nav-companion-bar>
|
||||||
|
<app-card-detail-layout
|
||||||
|
[isLoading]="isLoading"
|
||||||
|
[items]="filters"
|
||||||
|
[trackByIdentity]="trackByIdentity"
|
||||||
|
[jumpBarKeys]="jumpbarKeys"
|
||||||
|
>
|
||||||
|
<ng-template #cardItem let-item let-position="idx">
|
||||||
|
<!-- TODO: figure a way to get a hover effect -->
|
||||||
|
<div class="card-item-container card clickable" (click)="loadSmartFilter(item)">
|
||||||
|
<div class="overlay">
|
||||||
|
<div class="card-overlay"></div>
|
||||||
|
<div class="overlay-information overlay-information--centered">
|
||||||
|
<div class="position-relative">
|
||||||
|
<span class="card-title library mx-auto" style="width: auto;">
|
||||||
|
<i class="fa-solid fa-filter" style="font-size: 26px;" [ngClass]="{'error': isErrored(item)}" aria-hidden="true"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<span class="card-title">{{item.name}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</app-card-detail-layout>
|
||||||
|
|
||||||
|
</ng-container>
|
16
UI/Web/src/app/all-filters/all-filters.component.scss
Normal file
16
UI/Web/src/app/all-filters/all-filters.component.scss
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
.card-title {
|
||||||
|
width: 146px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: var(--error-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card, .overlay {
|
||||||
|
height: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-item-container .overlay-information.overlay-information--centered {
|
||||||
|
top: 54px;
|
||||||
|
left: 51px;
|
||||||
|
}
|
59
UI/Web/src/app/all-filters/all-filters.component.ts
Normal file
59
UI/Web/src/app/all-filters/all-filters.component.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, OnInit} from '@angular/core';
|
||||||
|
import {CommonModule} from '@angular/common';
|
||||||
|
import {JumpKey} from "../_models/jumpbar/jump-key";
|
||||||
|
import {EVENTS, Message, MessageHubService} from "../_services/message-hub.service";
|
||||||
|
import {TranslocoDirective} from "@ngneat/transloco";
|
||||||
|
import {CardItemComponent} from "../cards/card-item/card-item.component";
|
||||||
|
import {
|
||||||
|
SideNavCompanionBarComponent
|
||||||
|
} from "../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component";
|
||||||
|
import {SmartFilter} from "../_models/metadata/v2/smart-filter";
|
||||||
|
import {FilterService} from "../_services/filter.service";
|
||||||
|
import {CardDetailLayoutComponent} from "../cards/card-detail-layout/card-detail-layout.component";
|
||||||
|
import {SafeHtmlPipe} from "../_pipes/safe-html.pipe";
|
||||||
|
import {Router} from "@angular/router";
|
||||||
|
import {Series} from "../_models/series";
|
||||||
|
import {JumpbarService} from "../_services/jumpbar.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-all-filters',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, TranslocoDirective, CardItemComponent, SideNavCompanionBarComponent, CardDetailLayoutComponent, SafeHtmlPipe],
|
||||||
|
templateUrl: './all-filters.component.html',
|
||||||
|
styleUrl: './all-filters.component.scss',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class AllFiltersComponent implements OnInit {
|
||||||
|
private readonly cdRef = inject(ChangeDetectorRef);
|
||||||
|
private readonly jumpbarService = inject(JumpbarService);
|
||||||
|
private readonly hubService = inject(MessageHubService);
|
||||||
|
private readonly router = inject(Router);
|
||||||
|
private readonly filterService = inject(FilterService);
|
||||||
|
|
||||||
|
jumpbarKeys: Array<JumpKey> = [];
|
||||||
|
filters: SmartFilter[] = [];
|
||||||
|
isLoading = true;
|
||||||
|
trackByIdentity = (index: number, item: SmartFilter) => item.name;
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.filterService.getAllFilters().subscribe(filters => {
|
||||||
|
this.filters = filters;
|
||||||
|
this.jumpbarKeys = this.jumpbarService.getJumpKeys(this.filters, (s: Series) => s.name);
|
||||||
|
this.isLoading = false;
|
||||||
|
this.cdRef.markForCheck();
|
||||||
|
});
|
||||||
|
// this.hubService.messages$.pipe(debounceTime(6000), takeUntilDestroyed(this.destroyRef)).subscribe((event: Message<any>) => {
|
||||||
|
// if (event.event !== EVENTS.) return;
|
||||||
|
// this.loadPage();
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadSmartFilter(filter: SmartFilter) {
|
||||||
|
await this.router.navigateByUrl('all-series?' + filter.filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
isErrored(filter: SmartFilter) {
|
||||||
|
return !decodeURIComponent(filter.filter).includes('¦');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -44,7 +44,7 @@ import {SeriesFilterV2} from "../../../_models/metadata/v2/series-filter-v2";
|
|||||||
})
|
})
|
||||||
export class AllSeriesComponent implements OnInit {
|
export class AllSeriesComponent implements OnInit {
|
||||||
|
|
||||||
title: string = translate('all-series.title');
|
title!: string;
|
||||||
series: Series[] = [];
|
series: Series[] = [];
|
||||||
loadingSeries = false;
|
loadingSeries = false;
|
||||||
pagination: Pagination = new Pagination();
|
pagination: Pagination = new Pagination();
|
||||||
@ -128,6 +128,7 @@ export class AllSeriesComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
this.title = translate('all-series.title');
|
||||||
this.hubService.messages$.pipe(debounceTime(6000), takeUntilDestroyed(this.destroyRef)).subscribe((event: Message<any>) => {
|
this.hubService.messages$.pipe(debounceTime(6000), takeUntilDestroyed(this.destroyRef)).subscribe((event: Message<any>) => {
|
||||||
if (event.event !== EVENTS.SeriesAdded) return;
|
if (event.event !== EVENTS.SeriesAdded) return;
|
||||||
this.loadPage();
|
this.loadPage();
|
||||||
@ -166,7 +167,11 @@ export class AllSeriesComponent implements OnInit {
|
|||||||
loadPage() {
|
loadPage() {
|
||||||
this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterActiveCheck);
|
this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterActiveCheck);
|
||||||
this.loadingSeries = true;
|
this.loadingSeries = true;
|
||||||
this.title = this.route.snapshot.queryParamMap.get('title') || this.filter?.name || translate('all-series.title');
|
|
||||||
|
let filterName = this.route.snapshot.queryParamMap.get('name');
|
||||||
|
filterName = filterName ? filterName.split('<27>')[0] : null;
|
||||||
|
|
||||||
|
this.title = this.route.snapshot.queryParamMap.get('title') || filterName || this.filter?.name || translate('all-series.title');
|
||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
this.seriesService.getAllSeriesV2(undefined, undefined, this.filter!).pipe(take(1)).subscribe(series => {
|
this.seriesService.getAllSeriesV2(undefined, undefined, this.filter!).pipe(take(1)).subscribe(series => {
|
||||||
this.series = series.result;
|
this.series = series.result;
|
||||||
|
@ -6,22 +6,82 @@ import { AdminGuard } from './_guards/admin.guard';
|
|||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: 'admin',
|
path: '',
|
||||||
canActivate: [AdminGuard],
|
|
||||||
loadChildren: () => import('./_routes/admin-routing.module').then(m => m.routes)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'preferences',
|
|
||||||
canActivate: [AuthGuard],
|
canActivate: [AuthGuard],
|
||||||
loadChildren: () => import('./_routes/user-settings-routing.module').then(m => m.routes)
|
runGuardsAndResolvers: 'always',
|
||||||
},
|
children: [
|
||||||
{
|
{
|
||||||
path: 'collections',
|
path: 'admin',
|
||||||
loadChildren: () => import('./_routes/collections-routing.module').then(m => m.routes)
|
canActivate: [AdminGuard],
|
||||||
},
|
loadChildren: () => import('./_routes/admin-routing.module').then(m => m.routes)
|
||||||
{
|
},
|
||||||
path: 'lists',
|
{
|
||||||
loadChildren: () => import('./_routes/reading-list-routing.module').then(m => m.routes)
|
path: 'preferences',
|
||||||
|
loadChildren: () => import('./_routes/user-settings-routing.module').then(m => m.routes)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'collections',
|
||||||
|
loadChildren: () => import('./_routes/collections-routing.module').then(m => m.routes)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'lists',
|
||||||
|
loadChildren: () => import('./_routes/reading-list-routing.module').then(m => m.routes)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'announcements',
|
||||||
|
canActivate: [AdminGuard],
|
||||||
|
loadChildren: () => import('./_routes/announcements-routing.module').then(m => m.routes)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'bookmarks',
|
||||||
|
loadChildren: () => import('./_routes/bookmark-routing.module').then(m => m.routes)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'all-series',
|
||||||
|
loadChildren: () => import('./_routes/all-series-routing.module').then(m => m.routes)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'all-filters',
|
||||||
|
loadChildren: () => import('./_routes/all-filters-routing.module').then(m => m.routes)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'want-to-read',
|
||||||
|
loadChildren: () => import('./_routes/want-to-read-routing.module').then(m => m.routes)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'home',
|
||||||
|
loadChildren: () => import('./_routes/dashboard-routing.module').then(m => m.routes)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'library',
|
||||||
|
runGuardsAndResolvers: 'always',
|
||||||
|
canActivate: [AuthGuard, LibraryAccessGuard],
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: ':libraryId',
|
||||||
|
pathMatch: 'full',
|
||||||
|
loadChildren: () => import('./_routes/library-detail-routing.module').then(m => m.routes)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':libraryId/series/:seriesId',
|
||||||
|
pathMatch: 'full',
|
||||||
|
loadComponent: () => import('../app/series-detail/_components/series-detail/series-detail.component').then(c => c.SeriesDetailComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':libraryId/series/:seriesId/manga',
|
||||||
|
loadChildren: () => import('./_routes/manga-reader.router.module').then(m => m.routes)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':libraryId/series/:seriesId/book',
|
||||||
|
loadChildren: () => import('./_routes/book-reader.router.module').then(m => m.routes)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':libraryId/series/:seriesId/pdf',
|
||||||
|
loadChildren: () => import('./_routes/pdf-reader.router.module').then(m => m.routes)
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'registration',
|
path: 'registration',
|
||||||
@ -31,55 +91,6 @@ const routes: Routes = [
|
|||||||
path: 'login',
|
path: 'login',
|
||||||
loadChildren: () => import('./_routes/registration.router.module').then(m => m.routes) // TODO: Refactor so we just use /registration/login going forward
|
loadChildren: () => import('./_routes/registration.router.module').then(m => m.routes) // TODO: Refactor so we just use /registration/login going forward
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'announcements',
|
|
||||||
loadChildren: () => import('./_routes/announcements-routing.module').then(m => m.routes)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'bookmarks',
|
|
||||||
loadChildren: () => import('./_routes/bookmark-routing.module').then(m => m.routes)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'all-series',
|
|
||||||
loadChildren: () => import('./_routes/all-series-routing.module').then(m => m.routes)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'want-to-read',
|
|
||||||
loadChildren: () => import('./_routes/want-to-read-routing.module').then(m => m.routes)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'home',
|
|
||||||
loadChildren: () => import('./_routes/dashboard-routing.module').then(m => m.routes)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'library',
|
|
||||||
runGuardsAndResolvers: 'always',
|
|
||||||
canActivate: [AuthGuard, LibraryAccessGuard],
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: ':libraryId',
|
|
||||||
pathMatch: 'full',
|
|
||||||
loadChildren: () => import('./_routes/library-detail-routing.module').then(m => m.routes)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ':libraryId/series/:seriesId',
|
|
||||||
pathMatch: 'full',
|
|
||||||
loadComponent: () => import('../app/series-detail/_components/series-detail/series-detail.component').then(c => c.SeriesDetailComponent)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ':libraryId/series/:seriesId/manga',
|
|
||||||
loadChildren: () => import('./_routes/manga-reader.router.module').then(m => m.routes)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ':libraryId/series/:seriesId/book',
|
|
||||||
loadChildren: () => import('./_routes/book-reader.router.module').then(m => m.routes)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ':libraryId/series/:seriesId/pdf',
|
|
||||||
loadChildren: () => import('./_routes/pdf-reader.router.module').then(m => m.routes)
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{path: '**', pathMatch: 'full', redirectTo: 'home'},
|
{path: '**', pathMatch: 'full', redirectTo: 'home'},
|
||||||
{path: 'libraries', pathMatch: 'full', redirectTo: 'home'},
|
{path: 'libraries', pathMatch: 'full', redirectTo: 'home'},
|
||||||
{path: '**', pathMatch: 'prefix', redirectTo: 'home'},
|
{path: '**', pathMatch: 'prefix', redirectTo: 'home'},
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<div class="container-flex {{darkMode ? 'dark-mode' : ''}} reader-container {{ColumnLayout}} {{WritingStyleClass}}"
|
<div class="container-flex {{darkMode ? 'dark-mode' : ''}} reader-container {{ColumnLayout}} {{WritingStyleClass}}"
|
||||||
tabindex="0" #reader (click)="handleContainerClick($event)" [ngClass]="{'pointer' : cursorIsPointer}">
|
tabindex="0" #reader (click)="handleContainerClick($event)" [ngClass]="{'clickable' : cursorIsPointer}">
|
||||||
<ng-container *transloco="let t; read: 'book-reader'">
|
<ng-container *transloco="let t; read: 'book-reader'">
|
||||||
<div class="fixed-top" #stickyTop>
|
<div class="fixed-top" #stickyTop>
|
||||||
<a class="visually-hidden-focusable focus-visible" href="javascript:void(0);" (click)="moveFocus()">{{t('skip-header')}}</a>
|
<a class="visually-hidden-focusable focus-visible" href="javascript:void(0);" (click)="moveFocus()">{{t('skip-header')}}</a>
|
||||||
@ -44,7 +44,7 @@
|
|||||||
<button class="btn btn-small btn-icon col-1" [disabled]="prevChapterDisabled" (click)="loadPrevChapter()" [title]="t('prev-chapter')"><i class="fa fa-fast-backward" aria-hidden="true"></i></button>
|
<button class="btn btn-small btn-icon col-1" [disabled]="prevChapterDisabled" (click)="loadPrevChapter()" [title]="t('prev-chapter')"><i class="fa fa-fast-backward" aria-hidden="true"></i></button>
|
||||||
<div class="col-1" (click)="goToPage(0)">{{pageNum}}</div>
|
<div class="col-1" (click)="goToPage(0)">{{pageNum}}</div>
|
||||||
<div class="col-8">
|
<div class="col-8">
|
||||||
<ngb-progressbar style="cursor: pointer" [title]="t('go-to-page')" (click)="goToPage()" type="primary" height="5px" [value]="pageNum" [max]="maxPages - 1"></ngb-progressbar>
|
<ngb-progressbar class="clickable" [title]="t('go-to-page')" (click)="goToPage()" type="primary" height="5px" [value]="pageNum" [max]="maxPages - 1"></ngb-progressbar>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-1 btn-icon" (click)="goToPage(maxPages - 1)" [title]="t('go-to-last-page')">{{maxPages - 1}}</div>
|
<div class="col-1 btn-icon" (click)="goToPage(maxPages - 1)" [title]="t('go-to-last-page')">{{maxPages - 1}}</div>
|
||||||
<button class="btn btn-small btn-icon col-1" [disabled]="nextChapterDisabled" (click)="loadNextChapter()" [title]="t('next-chapter')"><i class="fa fa-fast-forward" aria-hidden="true"></i></button>
|
<button class="btn btn-small btn-icon col-1" [disabled]="nextChapterDisabled" (click)="loadNextChapter()" [title]="t('next-chapter')"><i class="fa fa-fast-forward" aria-hidden="true"></i></button>
|
||||||
|
@ -200,10 +200,6 @@ $action-bar-height: 38px;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.pointer {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.book-content {
|
.book-content {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 0 0;
|
margin: 0 0;
|
||||||
@ -398,6 +394,7 @@ $pagination-opacity: 0;
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.highlight {
|
.highlight {
|
||||||
background-color: rgba(65, 225, 100, 0.5) !important;
|
background-color: rgba(65, 225, 100, 0.5) !important;
|
||||||
animation: fadein .5s both;
|
animation: fadein .5s both;
|
||||||
@ -409,7 +406,7 @@ $pagination-opacity: 0;
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// TODO: Figure out why book-reader has it's own button overrides
|
||||||
.btn {
|
.btn {
|
||||||
&.btn-secondary {
|
&.btn-secondary {
|
||||||
color: var(--br-actionbar-button-text-color);
|
color: var(--br-actionbar-button-text-color);
|
||||||
|
@ -1614,13 +1614,12 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Responsibile for handling pagination only
|
// Responsible for handling pagination only
|
||||||
handleContainerClick(event: MouseEvent) {
|
handleContainerClick(event: MouseEvent) {
|
||||||
|
|
||||||
//if (event.target)
|
|
||||||
console.log('target: ', event.target);
|
console.log('target: ', event.target);
|
||||||
if (['action-bar'].some(className => (event.target as Element).classList.contains(className))) {
|
if (this.actionBarVisible || ['action-bar'].some(className => (event.target as Element).classList.contains(className))) {
|
||||||
console.log('exiting early')
|
//console.log('exiting early')
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,3 @@
|
|||||||
.clickable {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clickable:hover, .clickable:focus {
|
.clickable:hover, .clickable:focus {
|
||||||
background-color: lightgreen;
|
background-color: lightgreen;
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<ng-container *transloco="let t; read: 'card-item'">
|
<ng-container *transloco="let t; read: 'card-item'">
|
||||||
<div class="card {{selected ? 'selected-highlight' : ''}}">
|
<div class="card-item-container card {{selected ? 'selected-highlight' : ''}}">
|
||||||
<div class="overlay" (click)="handleClick($event)">
|
<div class="overlay" (click)="handleClick($event)">
|
||||||
<ng-container *ngIf="total > 0 || suppressArchiveWarning">
|
<ng-container *ngIf="total > 0 || suppressArchiveWarning">
|
||||||
<app-image borderRadius=".25rem .25rem 0 0" height="230px" width="158px" [imageUrl]="imageUrl"></app-image>
|
<app-image borderRadius=".25rem .25rem 0 0" height="230px" width="158px" [imageUrl]="imageUrl"></app-image>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="total === 0 && !suppressArchiveWarning">
|
<ng-container *ngIf="total === 0 && !suppressArchiveWarning">
|
||||||
<app-image borderRadius=".25rem .25rem 0 0" height="230px" width="158px" [imageUrl]="imageService.errorImage"></app-image>
|
<app-image borderRadius=".25rem .25rem 0 0" height="230px" width="158px" [imageUrl]="imageService.errorImage"></app-image>
|
||||||
@ -34,11 +34,14 @@
|
|||||||
<span class="badge bg-primary">{{count}}</span>
|
<span class="badge bg-primary">{{count}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-overlay"></div>
|
<div class="card-overlay"></div>
|
||||||
<div class="overlay-information {{centerOverlay ? 'overlay-information--centered' : ''}}" *ngIf="overlayInformation !== '' || overlayInformation !== undefined">
|
<ng-container *ngIf="overlayInformation | safeHtml as info">
|
||||||
<div class="position-relative">
|
<div class="overlay-information {{centerOverlay ? 'overlay-information--centered' : ''}}" *ngIf="info !== '' || info !== undefined">
|
||||||
<span class="card-title library mx-auto" style="width: auto;" [ngbTooltip]="overlayInformation" placement="top">{{overlayInformation}}</span>
|
<div class="position-relative">
|
||||||
|
<span class="card-title library mx-auto" style="width: auto;" [ngbTooltip]="info" placement="top" [innerHTML]="info"></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ng-container>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-body" *ngIf="title.length > 0 || actions.length > 0">
|
<div class="card-body" *ngIf="title.length > 0 || actions.length > 0">
|
||||||
@ -54,7 +57,7 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
{{title}}
|
{{title}}
|
||||||
</span>
|
</span>
|
||||||
<span class="card-actions float-end">
|
<span class="card-actions float-end" *ngIf="actions && actions.length > 0">
|
||||||
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="title"></app-card-actionables>
|
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="title"></app-card-actionables>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -17,39 +17,11 @@ $image-width: 160px;
|
|||||||
right: 0px;
|
right: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
|
||||||
max-width: $image-width;
|
|
||||||
cursor: pointer;
|
|
||||||
padding-left: 0px;
|
|
||||||
padding-right: 0px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
position: relative;
|
|
||||||
color: var(--card-text-color);
|
|
||||||
border: 1px var(--card-border-color);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-title {
|
|
||||||
font-size: 13px;
|
|
||||||
width: 130px;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
display: block;
|
|
||||||
margin-top: 2px;
|
|
||||||
margin-bottom: 0px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected-highlight {
|
.selected-highlight {
|
||||||
outline: 2px solid var(--primary-color);
|
outline: 2px solid var(--primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.img-top {
|
|
||||||
height: $image-height;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-banner {
|
.progress-banner {
|
||||||
width: $image-width;
|
width: $image-width;
|
||||||
height: 5px;
|
height: 5px;
|
||||||
@ -78,7 +50,6 @@ $image-width: 160px;
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
width: 158px;
|
width: 158px;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.not-read-badge {
|
.not-read-badge {
|
||||||
@ -113,46 +84,14 @@ $image-width: 160px;
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.overlay-information {
|
|
||||||
position: absolute;
|
|
||||||
top: 5px;
|
|
||||||
left: 5px;
|
|
||||||
border-radius: 15px;
|
|
||||||
padding: 0 10px;
|
|
||||||
background-color: var(--card-bg-color);
|
|
||||||
|
|
||||||
&.overlay-information--centered {
|
|
||||||
top: 95px;
|
|
||||||
left: 36px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.overlay {
|
.overlay {
|
||||||
height: $image-height;
|
|
||||||
border-top-left-radius: 4px;
|
|
||||||
border-top-right-radius: 4px;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
visibility: visible;
|
|
||||||
|
|
||||||
.bulk-mode {
|
.bulk-mode {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
z-index: 110;
|
z-index: 110;
|
||||||
}
|
}
|
||||||
|
|
||||||
.overlay-item {
|
|
||||||
visibility: visible;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.overlay-item {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
z-index: 10;
|
|
||||||
|
|
||||||
.count {
|
.count {
|
||||||
top: 5px;
|
top: 5px;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
@ -167,29 +106,8 @@ $image-width: 160px;
|
|||||||
width: 20px;
|
width: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-body {
|
|
||||||
padding: 5px !important;
|
|
||||||
background-color: var(--card-bg-color);
|
|
||||||
border-width: var(--card-border-width);
|
|
||||||
border-style: var(--card-border-style);
|
|
||||||
border-color: var(--card-border-color);
|
|
||||||
border-radius: 0.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.library {
|
.library {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-overlay {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: $image-height;
|
|
||||||
z-index: 10;
|
|
||||||
transition: all 0.2s;
|
|
||||||
border-top-left-radius: 4px;
|
|
||||||
border-top-right-radius: 4px;
|
|
||||||
}
|
|
||||||
|
@ -5,7 +5,7 @@ import {
|
|||||||
EventEmitter,
|
EventEmitter,
|
||||||
HostListener,
|
HostListener,
|
||||||
inject,
|
inject,
|
||||||
Input, NgZone,
|
Input,
|
||||||
OnInit,
|
OnInit,
|
||||||
Output
|
Output
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
@ -39,11 +39,11 @@ import {MangaFormatIconPipe} from "../../_pipes/manga-format-icon.pipe";
|
|||||||
import {SentenceCasePipe} from "../../_pipes/sentence-case.pipe";
|
import {SentenceCasePipe} from "../../_pipes/sentence-case.pipe";
|
||||||
import {CommonModule} from "@angular/common";
|
import {CommonModule} from "@angular/common";
|
||||||
import {RouterLink} from "@angular/router";
|
import {RouterLink} from "@angular/router";
|
||||||
import {translate, TranslocoModule, TranslocoService} from "@ngneat/transloco";
|
import {TranslocoModule} from "@ngneat/transloco";
|
||||||
import {CardActionablesComponent} from "../../_single-module/card-actionables/card-actionables.component";
|
import {CardActionablesComponent} from "../../_single-module/card-actionables/card-actionables.component";
|
||||||
import {NextExpectedChapter} from "../../_models/series-detail/next-expected-chapter";
|
import {NextExpectedChapter} from "../../_models/series-detail/next-expected-chapter";
|
||||||
import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe";
|
import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe";
|
||||||
import {TimeAgoPipe} from "../../_pipes/time-ago.pipe";
|
import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-card-item',
|
selector: 'app-card-item',
|
||||||
@ -60,7 +60,8 @@ import {TimeAgoPipe} from "../../_pipes/time-ago.pipe";
|
|||||||
CardActionablesComponent,
|
CardActionablesComponent,
|
||||||
SentenceCasePipe,
|
SentenceCasePipe,
|
||||||
RouterLink,
|
RouterLink,
|
||||||
TranslocoModule
|
TranslocoModule,
|
||||||
|
SafeHtmlPipe
|
||||||
],
|
],
|
||||||
templateUrl: './card-item.component.html',
|
templateUrl: './card-item.component.html',
|
||||||
styleUrls: ['./card-item.component.scss'],
|
styleUrls: ['./card-item.component.scss'],
|
||||||
@ -68,6 +69,19 @@ import {TimeAgoPipe} from "../../_pipes/time-ago.pipe";
|
|||||||
})
|
})
|
||||||
export class CardItemComponent implements OnInit {
|
export class CardItemComponent implements OnInit {
|
||||||
|
|
||||||
|
private readonly destroyRef = inject(DestroyRef);
|
||||||
|
public readonly imageService = inject(ImageService);
|
||||||
|
public readonly bulkSelectionService = inject(BulkSelectionService);
|
||||||
|
private readonly libraryService = inject(LibraryService);
|
||||||
|
private readonly downloadService = inject(DownloadService);
|
||||||
|
private readonly utilityService = inject(UtilityService);
|
||||||
|
private readonly messageHub = inject(MessageHubService);
|
||||||
|
private readonly accountService = inject(AccountService);
|
||||||
|
private readonly scrollService = inject(ScrollService);
|
||||||
|
private readonly cdRef = inject(ChangeDetectorRef);
|
||||||
|
private readonly actionFactoryService = inject(ActionFactoryService);
|
||||||
|
protected readonly MangaFormat = MangaFormat;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Card item url. Will internally handle error and missing covers
|
* Card item url. Will internally handle error and missing covers
|
||||||
*/
|
*/
|
||||||
@ -109,7 +123,7 @@ export class CardItemComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
@Input() allowSelection: boolean = false;
|
@Input() allowSelection: boolean = false;
|
||||||
/**
|
/**
|
||||||
* This will suppress the cannot read archive warning when total pages is 0
|
* This will suppress the "cannot read archive warning" when total pages is 0
|
||||||
*/
|
*/
|
||||||
@Input() suppressArchiveWarning: boolean = false;
|
@Input() suppressArchiveWarning: boolean = false;
|
||||||
/**
|
/**
|
||||||
@ -159,21 +173,6 @@ export class CardItemComponent implements OnInit {
|
|||||||
selectionInProgress: boolean = false;
|
selectionInProgress: boolean = false;
|
||||||
|
|
||||||
private user: User | undefined;
|
private user: User | undefined;
|
||||||
private readonly destroyRef = inject(DestroyRef);
|
|
||||||
private readonly ngZone = inject(NgZone);
|
|
||||||
private readonly translocoService = inject(TranslocoService);
|
|
||||||
|
|
||||||
get MangaFormat(): typeof MangaFormat {
|
|
||||||
return MangaFormat;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
constructor(public imageService: ImageService, private libraryService: LibraryService,
|
|
||||||
public utilityService: UtilityService, private downloadService: DownloadService,
|
|
||||||
public bulkSelectionService: BulkSelectionService,
|
|
||||||
private messageHub: MessageHubService, private accountService: AccountService,
|
|
||||||
private scrollService: ScrollService, private readonly cdRef: ChangeDetectorRef,
|
|
||||||
private actionFactoryService: ActionFactoryService) {}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
|
||||||
@ -224,12 +223,15 @@ export class CardItemComponent implements OnInit {
|
|||||||
this.imageUrl = '';
|
this.imageUrl = '';
|
||||||
const nextDate = (this.entity as NextExpectedChapter);
|
const nextDate = (this.entity as NextExpectedChapter);
|
||||||
|
|
||||||
this.overlayInformation = nextDate.title;
|
const tokens = nextDate.title.split(':');
|
||||||
|
this.overlayInformation = `
|
||||||
|
<i class="fa-regular fa-clock mb-2" style="font-size: 26px" aria-hidden="true"></i>
|
||||||
|
<div>${tokens[0]}</div><div>${tokens[1]}</div>`;
|
||||||
this.centerOverlay = true;
|
this.centerOverlay = true;
|
||||||
|
|
||||||
if (nextDate.expectedDate) {
|
if (nextDate.expectedDate) {
|
||||||
const utcPipe = new UtcToLocalTimePipe();
|
const utcPipe = new UtcToLocalTimePipe();
|
||||||
this.title = utcPipe.transform(nextDate.expectedDate, 'shortDate');
|
this.title = '~ ' + utcPipe.transform(nextDate.expectedDate, 'shortDate');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
|
@ -52,7 +52,7 @@
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="row g-0 chooser" style="padding-top: 10px">
|
<div class="row g-0 chooser" style="padding-top: 10px">
|
||||||
<div class="image-card col-auto"
|
<div class="clickable col-auto"
|
||||||
*ngIf="showReset" tabindex="0" (click)="reset()"
|
*ngIf="showReset" tabindex="0" (click)="reset()"
|
||||||
[ngClass]="{'selected': !showApplyButton && selectedIndex === -1}">
|
[ngClass]="{'selected': !showApplyButton && selectedIndex === -1}">
|
||||||
<app-image class="card-img-top" [title]="t('reset-cover-tooltip')" height="230px" width="158px" [imageUrl]="imageService.resetCoverImage"></app-image>
|
<app-image class="card-img-top" [title]="t('reset-cover-tooltip')" height="230px" width="158px" [imageUrl]="imageService.resetCoverImage"></app-image>
|
||||||
@ -62,7 +62,7 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="image-card col-auto"
|
<div class="clickable col-auto"
|
||||||
*ngFor="let url of imageUrls; let idx = index;" tabindex="0" [attr.aria-label]="t('image-num', {num: idx + 1})" (click)="selectImage(idx)"
|
*ngFor="let url of imageUrls; let idx = index;" tabindex="0" [attr.aria-label]="t('image-num', {num: idx + 1})" (click)="selectImage(idx)"
|
||||||
[ngClass]="{'selected': !showApplyButton && selectedIndex === idx}">
|
[ngClass]="{'selected': !showApplyButton && selectedIndex === idx}">
|
||||||
<app-image class="card-img-top" height="230px" width="158px" [imageUrl]="url" [processEvents]="idx > 0"></app-image>
|
<app-image class="card-img-top" height="230px" width="158px" [imageUrl]="url" [processEvents]="idx > 0"></app-image>
|
||||||
|
@ -7,9 +7,6 @@ $image-width: 160px;
|
|||||||
max-height: $image-height;
|
max-height: $image-height;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-card {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected {
|
.selected {
|
||||||
outline: 5px solid var(--primary-color);
|
outline: 5px solid var(--primary-color);
|
||||||
@ -38,4 +35,4 @@ ngx-file-drop ::ng-deep > div {
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<ng-container *transloco="let t; read: 'external-series-card'">
|
<ng-container *transloco="let t; read: 'external-series-card'">
|
||||||
<ng-container *ngIf="data !== undefined">
|
<ng-container *ngIf="data !== undefined">
|
||||||
<div class="card">
|
<div class="card-item-container card clickable">
|
||||||
<div class="overlay" (click)="handleClick()">
|
<div class="overlay" (click)="handleClick()">
|
||||||
<ng-container>
|
<ng-container>
|
||||||
<app-image borderRadius=".25rem .25rem 0 0" height="230px" width="158px" [imageUrl]="data.coverUrl"></app-image>
|
<app-image borderRadius=".25rem .25rem 0 0" height="230px" width="158px" [imageUrl]="data.coverUrl"></app-image>
|
||||||
|
@ -1,126 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
$image-height: 230px;
|
|
||||||
$image-width: 160px;
|
|
||||||
|
|
||||||
|
|
||||||
.card {
|
|
||||||
max-width: $image-width;
|
|
||||||
cursor: pointer;
|
|
||||||
padding-left: 0px;
|
|
||||||
padding-right: 0px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
position: relative;
|
|
||||||
color: var(--card-text-color);
|
|
||||||
border: 1px var(--card-border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-actions {
|
|
||||||
position: absolute;
|
|
||||||
top: 236px;
|
|
||||||
right: 0px;
|
|
||||||
width: 20px;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.library {
|
|
||||||
font-size: 13px;
|
|
||||||
text-decoration: none;
|
|
||||||
margin-top: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-title {
|
|
||||||
font-size: 13px;
|
|
||||||
width: 131px;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
display: block;
|
|
||||||
margin-top: 2px;
|
|
||||||
margin-bottom: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.img-top {
|
|
||||||
height: $image-height;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.badge-container {
|
|
||||||
border-radius: 4px;
|
|
||||||
display: block;
|
|
||||||
height: $image-height;
|
|
||||||
left: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
pointer-events: none;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
width: 158px;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.not-read-badge {
|
|
||||||
position: absolute;
|
|
||||||
top: calc(-1 * (var(--card-progress-triangle-size) / 2));
|
|
||||||
right: -14px;
|
|
||||||
z-index: 1000;
|
|
||||||
height: var(--card-progress-triangle-size);
|
|
||||||
width: var(--card-progress-triangle-size);
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
transform: rotate(45deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.overlay {
|
|
||||||
height: $image-height;
|
|
||||||
border-top-left-radius: 4px;
|
|
||||||
border-top-right-radius: 4px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
visibility: visible;
|
|
||||||
|
|
||||||
.bulk-mode {
|
|
||||||
visibility: visible;
|
|
||||||
z-index: 110;
|
|
||||||
}
|
|
||||||
|
|
||||||
.overlay-item {
|
|
||||||
visibility: visible;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.overlay-item {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
z-index: 10;
|
|
||||||
|
|
||||||
.count {
|
|
||||||
top: 5px;
|
|
||||||
right: 10px;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.card-body {
|
|
||||||
padding: 5px !important;
|
|
||||||
background-color: var(--card-bg-color);
|
|
||||||
border-width: var(--card-border-width);
|
|
||||||
border-style: var(--card-border-style);
|
|
||||||
border-color: var(--card-border-color);
|
|
||||||
border-radius: 0.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-overlay {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: $image-height;
|
|
||||||
z-index: 10;
|
|
||||||
transition: all 0.2s;
|
|
||||||
border-top-left-radius: 4px;
|
|
||||||
border-top-right-radius: 4px;
|
|
||||||
}
|
|
@ -9,11 +9,10 @@ import {CommonModule} from '@angular/common';
|
|||||||
import {ExternalSeries} from "../../_models/series-detail/external-series";
|
import {ExternalSeries} from "../../_models/series-detail/external-series";
|
||||||
import {RouterLinkActive} from "@angular/router";
|
import {RouterLinkActive} from "@angular/router";
|
||||||
import {ImageComponent} from "../../shared/image/image.component";
|
import {ImageComponent} from "../../shared/image/image.component";
|
||||||
import {NgbActiveOffcanvas, NgbOffcanvas, NgbProgressbar, NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
|
import {NgbOffcanvas, NgbProgressbar, NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
|
||||||
import {ReactiveFormsModule} from "@angular/forms";
|
import {ReactiveFormsModule} from "@angular/forms";
|
||||||
import {TranslocoDirective} from "@ngneat/transloco";
|
import {TranslocoDirective} from "@ngneat/transloco";
|
||||||
import {SeriesPreviewDrawerComponent} from "../../_single-module/series-preview-drawer/series-preview-drawer.component";
|
import {SeriesPreviewDrawerComponent} from "../../_single-module/series-preview-drawer/series-preview-drawer.component";
|
||||||
import {SeriesService} from "../../_services/series.service";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-external-series-card',
|
selector: 'app-external-series-card',
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
<div class="card-item-container card">
|
||||||
|
<div class="overlay">
|
||||||
|
<app-image borderRadius=".25rem .25rem 0 0" height="230px" width="158px" classes="extreme-blur" [imageUrl]="imageUrl"></app-image>
|
||||||
|
|
||||||
|
<div class="card-overlay"></div>
|
||||||
|
<ng-container *ngIf="overlayInformation | safeHtml as info">
|
||||||
|
<div class="overlay-information overlay-information--centered" *ngIf="info !== '' || info !== undefined">
|
||||||
|
<div class="position-relative">
|
||||||
|
<span class="card-title library mx-auto" style="width: auto;">
|
||||||
|
<i class="fa-regular fa-clock mb-2" style="font-size: 26px" aria-hidden="true"></i>
|
||||||
|
<span [innerHTML]="info"></span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<span class="card-title" tabindex="0">{{title}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,11 @@
|
|||||||
|
::ng-deep .extreme-blur {
|
||||||
|
filter: brightness(50%) blur(4px)
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-information {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
width: 146px;
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input} from '@angular/core';
|
||||||
|
import {CommonModule} from '@angular/common';
|
||||||
|
import {ImageComponent} from "../../shared/image/image.component";
|
||||||
|
import {NextExpectedChapter} from "../../_models/series-detail/next-expected-chapter";
|
||||||
|
import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe";
|
||||||
|
import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-next-expected-card',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, ImageComponent, SafeHtmlPipe],
|
||||||
|
templateUrl: './next-expected-card.component.html',
|
||||||
|
styleUrl: './next-expected-card.component.scss',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class NextExpectedCardComponent {
|
||||||
|
private readonly cdRef = inject(ChangeDetectorRef);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Card item url. Will internally handle error and missing covers
|
||||||
|
*/
|
||||||
|
@Input() imageUrl = '';
|
||||||
|
/**
|
||||||
|
* This is the entity we are representing. It will be returned if an action is executed.
|
||||||
|
*/
|
||||||
|
@Input({required: true}) entity!: NextExpectedChapter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional information to show on the overlay area. Will always render.
|
||||||
|
*/
|
||||||
|
@Input() overlayInformation: string = '';
|
||||||
|
title: string = '';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
const tokens = this.entity.title.split(':');
|
||||||
|
this.overlayInformation = `<div>${tokens[0]}</div><div>${tokens[1]}</div>`;
|
||||||
|
|
||||||
|
if (this.entity.expectedDate) {
|
||||||
|
const utcPipe = new UtcToLocalTimePipe();
|
||||||
|
this.title = '~ ' + utcPipe.transform(this.entity.expectedDate, 'shortDate');
|
||||||
|
}
|
||||||
|
this.cdRef.markForCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -318,6 +318,7 @@ export class MetadataFilterRowComponent implements OnInit {
|
|||||||
|
|
||||||
if (this.loaded) {
|
if (this.loaded) {
|
||||||
this.formGroup.get('filterValue')?.patchValue('');
|
this.formGroup.get('filterValue')?.patchValue('');
|
||||||
|
this.formGroup.get('comparison')?.patchValue(StringComparisons[0]);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -329,7 +330,10 @@ export class MetadataFilterRowComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
this.validComparisons$.next(comps);
|
this.validComparisons$.next(comps);
|
||||||
this.predicateType$.next(PredicateType.Number);
|
this.predicateType$.next(PredicateType.Number);
|
||||||
if (this.loaded) this.formGroup.get('filterValue')?.patchValue(0);
|
if (this.loaded) {
|
||||||
|
this.formGroup.get('filterValue')?.patchValue(0);
|
||||||
|
this.formGroup.get('comparison')?.patchValue(NumberFields[0]);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -339,6 +343,7 @@ export class MetadataFilterRowComponent implements OnInit {
|
|||||||
|
|
||||||
if (this.loaded) {
|
if (this.loaded) {
|
||||||
this.formGroup.get('filterValue')?.patchValue(false);
|
this.formGroup.get('filterValue')?.patchValue(false);
|
||||||
|
this.formGroup.get('comparison')?.patchValue(DateComparisons[0]);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -349,6 +354,7 @@ export class MetadataFilterRowComponent implements OnInit {
|
|||||||
|
|
||||||
if (this.loaded) {
|
if (this.loaded) {
|
||||||
this.formGroup.get('filterValue')?.patchValue(false);
|
this.formGroup.get('filterValue')?.patchValue(false);
|
||||||
|
this.formGroup.get('comparison')?.patchValue(BooleanComparisons[0]);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -363,7 +369,10 @@ export class MetadataFilterRowComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
this.validComparisons$.next(comps);
|
this.validComparisons$.next(comps);
|
||||||
this.predicateType$.next(PredicateType.Dropdown);
|
this.predicateType$.next(PredicateType.Dropdown);
|
||||||
if (this.loaded) this.formGroup.get('filterValue')?.patchValue(0);
|
if (this.loaded) {
|
||||||
|
this.formGroup.get('filterValue')?.patchValue(0);
|
||||||
|
this.formGroup.get('comparison')?.patchValue(comps[0]);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -163,6 +163,7 @@
|
|||||||
<div ngbDropdownMenu>
|
<div ngbDropdownMenu>
|
||||||
<a class="xs-only" ngbDropdownItem routerLink="/admin/dashboard" *ngIf="user.roles.includes('Admin')">{{t('server-settings')}}</a>
|
<a class="xs-only" ngbDropdownItem routerLink="/admin/dashboard" *ngIf="user.roles.includes('Admin')">{{t('server-settings')}}</a>
|
||||||
<a ngbDropdownItem routerLink="/preferences/">{{t('settings')}}</a>
|
<a ngbDropdownItem routerLink="/preferences/">{{t('settings')}}</a>
|
||||||
|
<a ngbDropdownItem routerLink="/all-filters/">{{t('all-filters')}}</a>
|
||||||
<a ngbDropdownItem href="https://wiki.kavitareader.com" rel="noopener noreferrer" target="_blank">{{t('help')}}</a>
|
<a ngbDropdownItem href="https://wiki.kavitareader.com" rel="noopener noreferrer" target="_blank">{{t('help')}}</a>
|
||||||
<a ngbDropdownItem routerLink="/announcements/" *ngIf="accountService.hasAdminRole(user)">{{t('announcements')}}</a>
|
<a ngbDropdownItem routerLink="/announcements/" *ngIf="accountService.hasAdminRole(user)">{{t('announcements')}}</a>
|
||||||
<a ngbDropdownItem (click)="logout()">{{t('logout')}}</a>
|
<a ngbDropdownItem (click)="logout()">{{t('logout')}}</a>
|
||||||
|
@ -288,13 +288,15 @@
|
|||||||
<ng-container *ngIf="nextExpectedChapter">
|
<ng-container *ngIf="nextExpectedChapter">
|
||||||
<ng-container [ngSwitch]="tabId">
|
<ng-container [ngSwitch]="tabId">
|
||||||
<ng-container *ngSwitchCase="TabID.Volumes">
|
<ng-container *ngSwitchCase="TabID.Volumes">
|
||||||
<app-card-item *ngIf="nextExpectedChapter.volumeNumber > 0 && nextExpectedChapter.chapterNumber === 0" class="col-auto mt-2 mb-2" [entity]="nextExpectedChapter"></app-card-item>
|
<app-next-expected-card *ngIf="nextExpectedChapter.volumeNumber > 0 && nextExpectedChapter.chapterNumber === 0"
|
||||||
|
class="col-auto mt-2 mb-2" [entity]="nextExpectedChapter"
|
||||||
|
[imageUrl]="imageService.getSeriesCoverImage(series.id)"></app-next-expected-card>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngSwitchCase="TabID.Chapters">
|
<ng-container *ngSwitchCase="TabID.Chapters">
|
||||||
<app-card-item class="col-auto mt-2 mb-2" [entity]="nextExpectedChapter" ></app-card-item>
|
<app-next-expected-card class="col-auto mt-2 mb-2" [entity]="nextExpectedChapter" [imageUrl]="imageService.getSeriesCoverImage(series.id)"></app-next-expected-card>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngSwitchCase="TabID.Storyline">
|
<ng-container *ngSwitchCase="TabID.Storyline">
|
||||||
<app-card-item class="col-auto mt-2 mb-2" [entity]="nextExpectedChapter" ></app-card-item>
|
<app-next-expected-card class="col-auto mt-2 mb-2" [entity]="nextExpectedChapter" [imageUrl]="imageService.getSeriesCoverImage(series.id)"></app-next-expected-card>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@ -93,6 +93,7 @@ import {
|
|||||||
} from "../../../_single-module/series-preview-drawer/series-preview-drawer.component";
|
} from "../../../_single-module/series-preview-drawer/series-preview-drawer.component";
|
||||||
import {PublicationStatus} from "../../../_models/metadata/publication-status";
|
import {PublicationStatus} from "../../../_models/metadata/publication-status";
|
||||||
import {NextExpectedChapter} from "../../../_models/series-detail/next-expected-chapter";
|
import {NextExpectedChapter} from "../../../_models/series-detail/next-expected-chapter";
|
||||||
|
import {NextExpectedCardComponent} from "../../../cards/next-expected-card/next-expected-card.component";
|
||||||
|
|
||||||
interface RelatedSeriesPair {
|
interface RelatedSeriesPair {
|
||||||
series: Series;
|
series: Series;
|
||||||
@ -120,7 +121,7 @@ interface StoryLineItem {
|
|||||||
styleUrls: ['./series-detail.component.scss'],
|
styleUrls: ['./series-detail.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [NgIf, SideNavCompanionBarComponent, CardActionablesComponent, ReactiveFormsModule, NgStyle, TagBadgeComponent, ImageComponent, NgbTooltip, NgbProgressbar, NgbDropdown, NgbDropdownToggle, NgbDropdownMenu, NgbDropdownItem, SeriesMetadataDetailComponent, CarouselReelComponent, ReviewCardComponent, BulkOperationsComponent, NgbNav, NgbNavItem, NgbNavLink, NgbNavContent, VirtualScrollerModule, NgFor, CardItemComponent, ListItemComponent, EntityTitleComponent, SeriesCardComponent, ExternalSeriesCardComponent, ExternalListItemComponent, NgbNavOutlet, LoadingComponent, DecimalPipe, TranslocoDirective, NgTemplateOutlet, NgSwitch, NgSwitchCase]
|
imports: [NgIf, SideNavCompanionBarComponent, CardActionablesComponent, ReactiveFormsModule, NgStyle, TagBadgeComponent, ImageComponent, NgbTooltip, NgbProgressbar, NgbDropdown, NgbDropdownToggle, NgbDropdownMenu, NgbDropdownItem, SeriesMetadataDetailComponent, CarouselReelComponent, ReviewCardComponent, BulkOperationsComponent, NgbNav, NgbNavItem, NgbNavLink, NgbNavContent, VirtualScrollerModule, NgFor, CardItemComponent, ListItemComponent, EntityTitleComponent, SeriesCardComponent, ExternalSeriesCardComponent, ExternalListItemComponent, NgbNavOutlet, LoadingComponent, DecimalPipe, TranslocoDirective, NgTemplateOutlet, NgSwitch, NgSwitchCase, NextExpectedCardComponent]
|
||||||
})
|
})
|
||||||
export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ import { AccountService } from 'src/app/_services/account.service';
|
|||||||
import { BytesPipe } from 'src/app/_pipes/bytes.pipe';
|
import { BytesPipe } from 'src/app/_pipes/bytes.pipe';
|
||||||
import {translate} from "@ngneat/transloco";
|
import {translate} from "@ngneat/transloco";
|
||||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||||
|
import {SAVER, Saver} from "../../_providers/saver.provider";
|
||||||
|
|
||||||
export const DEBOUNCE_TIME = 100;
|
export const DEBOUNCE_TIME = 100;
|
||||||
|
|
||||||
@ -66,9 +67,11 @@ export class DownloadService {
|
|||||||
public activeDownloads$ = this.downloadsSource.asObservable();
|
public activeDownloads$ = this.downloadsSource.asObservable();
|
||||||
|
|
||||||
private readonly destroyRef = inject(DestroyRef);
|
private readonly destroyRef = inject(DestroyRef);
|
||||||
|
private readonly confirmService = inject(ConfirmService);
|
||||||
|
private readonly accountService = inject(AccountService);
|
||||||
|
private readonly httpClient = inject(HttpClient);
|
||||||
|
|
||||||
constructor(private httpClient: HttpClient, private confirmService: ConfirmService,
|
constructor(@Inject(SAVER) private save: Saver) { }
|
||||||
private accountService: AccountService) { }
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -269,22 +272,4 @@ export class DownloadService {
|
|||||||
finalize(() => this.finalizeDownloadState(downloadType, subtitle))
|
finalize(() => this.finalizeDownloadState(downloadType, subtitle))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private save(blob: Blob, filename: string) {
|
|
||||||
const saveLink = document.createElement('a');
|
|
||||||
saveLink.style.display = 'none';
|
|
||||||
document.body.appendChild(saveLink);
|
|
||||||
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
saveLink.href = url;
|
|
||||||
saveLink.download = filename;
|
|
||||||
|
|
||||||
// Trigger the click event
|
|
||||||
saveLink.click();
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
document.body.removeChild(saveLink);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
<li class="list-group-item d-flex justify-content-between align-items-center" *ngFor="let item of items | filter: filterList; let i = index">
|
<li class="list-group-item d-flex justify-content-between align-items-center clickable" *ngFor="let item of items | filter: filterList; let i = index">
|
||||||
{{item}}
|
{{item}}
|
||||||
<button class="btn btn-primary" *ngIf="clicked !== undefined" (click)="handleClick(item)">
|
<button class="btn btn-primary" *ngIf="clicked !== undefined" (click)="handleClick(item)">
|
||||||
<i class="fa-solid fa-arrow-up-right-from-square" aria-hidden="true"></i>
|
<i class="fa-solid fa-arrow-up-right-from-square" aria-hidden="true"></i>
|
||||||
|
@ -1,7 +1,3 @@
|
|||||||
.list-group-item.no-click {
|
.list-group-item.no-click {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-group-item {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
@ -530,6 +530,11 @@
|
|||||||
"series-count": "{{common.series-count}}"
|
"series-count": "{{common.series-count}}"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"all-filters": {
|
||||||
|
"title": "All Smart Filters",
|
||||||
|
"count": "{{count}} {{customize-dashboard-modal.title-smart-filters}}"
|
||||||
|
},
|
||||||
|
|
||||||
"announcements": {
|
"announcements": {
|
||||||
"title": "Announcements"
|
"title": "Announcements"
|
||||||
},
|
},
|
||||||
@ -1411,7 +1416,8 @@
|
|||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"help": "Help",
|
"help": "Help",
|
||||||
"announcements": "Announcements",
|
"announcements": "Announcements",
|
||||||
"logout": "Logout"
|
"logout": "Logout",
|
||||||
|
"all-filters": "Smart Filters"
|
||||||
},
|
},
|
||||||
|
|
||||||
"add-to-list-modal": {
|
"add-to-list-modal": {
|
||||||
|
@ -27,6 +27,7 @@ import {switchMap} from "rxjs";
|
|||||||
import {provideTranslocoLocale} from "@ngneat/transloco-locale";
|
import {provideTranslocoLocale} from "@ngneat/transloco-locale";
|
||||||
import {provideTranslocoPersistTranslations} from "@ngneat/transloco-persist-translations";
|
import {provideTranslocoPersistTranslations} from "@ngneat/transloco-persist-translations";
|
||||||
import {LazyLoadImageModule} from "ng-lazyload-image";
|
import {LazyLoadImageModule} from "ng-lazyload-image";
|
||||||
|
import {getSaver, SAVER} from "./app/_providers/saver.provider";
|
||||||
|
|
||||||
const disableAnimations = !('animate' in document.documentElement);
|
const disableAnimations = !('animate' in document.documentElement);
|
||||||
|
|
||||||
@ -146,6 +147,7 @@ bootstrapApplication(AppComponent, {
|
|||||||
{ provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true },
|
{ provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true },
|
||||||
preLoad,
|
preLoad,
|
||||||
Title,
|
Title,
|
||||||
|
{ provide: SAVER, useFactory: getSaver },
|
||||||
provideHttpClient(withInterceptorsFromDi())
|
provideHttpClient(withInterceptorsFromDi())
|
||||||
]
|
]
|
||||||
} as ApplicationConfig)
|
} as ApplicationConfig)
|
||||||
|
@ -12,4 +12,81 @@
|
|||||||
background-color: var(--card-overlay-hover-bg-color);
|
background-color: var(--card-overlay-hover-bg-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$image-height: 230px;
|
||||||
|
$image-width: 160px;
|
||||||
|
|
||||||
|
.card-item-container {
|
||||||
|
.card {
|
||||||
|
max-width: $image-width;
|
||||||
|
cursor: pointer;
|
||||||
|
padding-left: 0px;
|
||||||
|
padding-right: 0px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
color: var(--card-text-color);
|
||||||
|
border: 1px var(--card-border-color);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 13px;
|
||||||
|
width: 130px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
display: block;
|
||||||
|
margin-top: 2px;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-information {
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
left: 5px;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 0 10px;
|
||||||
|
background-color: var(--card-bg-color);
|
||||||
|
|
||||||
|
&.overlay-information--centered {
|
||||||
|
top: 95px;
|
||||||
|
left: 36px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
height: $image-height;
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
padding: 5px !important;
|
||||||
|
background-color: var(--card-bg-color);
|
||||||
|
border-width: var(--card-border-width);
|
||||||
|
border-style: var(--card-border-style);
|
||||||
|
border-color: var(--card-border-color);
|
||||||
|
border-radius: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.card-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: $image-height;
|
||||||
|
z-index: 10;
|
||||||
|
transition: all 0.2s;
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -31,24 +31,18 @@ Build()
|
|||||||
|
|
||||||
BuildUI()
|
BuildUI()
|
||||||
{
|
{
|
||||||
ProgressStart 'Building UI'
|
|
||||||
cd ../Kavita-webui/ || exit
|
|
||||||
npm install
|
|
||||||
npm run prod
|
|
||||||
cd ../Kavita/ || exit
|
|
||||||
ProgressEnd 'Building UI'
|
|
||||||
|
|
||||||
ProgressStart 'Building UI'
|
ProgressStart 'Building UI'
|
||||||
echo 'Removing old wwwroot'
|
echo 'Removing old wwwroot'
|
||||||
rm -rf API/wwwroot/*
|
rm -rf API/wwwroot/*
|
||||||
cd ../Kavita-webui/ || exit
|
cd UI/Web/ || exit
|
||||||
echo 'Installing web dependencies'
|
echo 'Installing web dependencies'
|
||||||
npm install
|
npm install --legacy-peer-deps
|
||||||
echo 'Building UI'
|
echo 'Building UI'
|
||||||
npm run prod
|
npm run prod
|
||||||
echo 'Copying back to Kavita wwwroot'
|
echo 'Copying back to Kavita wwwroot'
|
||||||
cp -r dist/browser/* ../Kavita/API/wwwroot
|
mkdir -p ../../API/wwwroot
|
||||||
cd ../Kavita/ || exit
|
cp -R dist/browser/* ../../API/wwwroot
|
||||||
|
cd ../../ || exit
|
||||||
ProgressEnd 'Building UI'
|
ProgressEnd 'Building UI'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
0
identifier.sqlite
Normal file
0
identifier.sqlite
Normal file
@ -7,7 +7,7 @@
|
|||||||
"name": "GPL-3.0",
|
"name": "GPL-3.0",
|
||||||
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
||||||
},
|
},
|
||||||
"version": "0.7.10.8"
|
"version": "0.7.10.9"
|
||||||
},
|
},
|
||||||
"servers": [
|
"servers": [
|
||||||
{
|
{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user