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 }}
|
||||
shell: powershell
|
||||
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
|
||||
.\.sonar\scanner\dotnet-sonarscanner end /d:sonar.login="${{ secrets.SONAR_TOKEN }}"
|
||||
.\.sonar\scanner\dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}"
|
||||
|
||||
- name: Test
|
||||
run: dotnet test --no-restore --verbosity normal
|
||||
|
@ -7,11 +7,11 @@
|
||||
|
||||
<ItemGroup>
|
||||
<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="System.IO.Abstractions.TestingHelpers" 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">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
@ -12,6 +12,18 @@ namespace API.Tests.Helpers;
|
||||
|
||||
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]
|
||||
public void Test_Decode()
|
||||
{
|
||||
|
@ -94,13 +94,13 @@
|
||||
<PackageReference Include="NetVips" Version="2.3.1" />
|
||||
<PackageReference Include="NetVips.Native" Version="8.14.5" />
|
||||
<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.Enrichers.Thread" Version="3.2.0-dev-00752" />
|
||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="7.0.0" />
|
||||
<PackageReference Include="Serilog.Settings.Configuration" Version="7.0.1" />
|
||||
<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.SignalR.Core" Version="0.1.2" />
|
||||
<PackageReference Include="SharpCompress" Version="0.34.1" />
|
||||
|
@ -27,7 +27,7 @@ public static class MigrateSmartFilterEncoding
|
||||
var smartFilters = dataContext.AppUserSmartFilter.ToList();
|
||||
foreach (var filter in smartFilters)
|
||||
{
|
||||
if (filter.Filter.Contains(SmartFilterHelper.StatementSeparator)) continue;
|
||||
if (!ShouldMigrateFilter(filter.Filter)) continue;
|
||||
var decode = EncodeFix(filter.Filter);
|
||||
if (string.IsNullOrEmpty(decode)) continue;
|
||||
filter.Filter = decode;
|
||||
@ -41,6 +41,11 @@ public static class MigrateSmartFilterEncoding
|
||||
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)
|
||||
{
|
||||
var statements = StatementsRegex.Matches(encodedFilter)
|
||||
@ -67,6 +72,7 @@ public static class MigrateSmartFilterEncoding
|
||||
return $"sortField={sortFieldValue}{SmartFilterHelper.InnerStatementSeparator}isAscending={isAscendingValue}";
|
||||
});
|
||||
|
||||
//name=Zero&sortOptions=sortField=2&isAscending=False&limitTo=0&combination=1
|
||||
var filterDto = SmartFilterHelper.Decode(noStmt);
|
||||
|
||||
// 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",
|
||||
"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}",
|
||||
|
@ -745,13 +745,14 @@ public class SeriesService : ISeriesService
|
||||
result.VolumeNumber = lastChapter.Volume.Number;
|
||||
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}),
|
||||
LibraryType.Comic => await _localizationService.Translate(userId, "issue-num",
|
||||
LibraryType.Comic => await _localizationService.Translate(userId, "next-issue-num",
|
||||
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}),
|
||||
_ => "Chapter " + result.ChapterNumber
|
||||
_ => await _localizationService.Translate(userId, "next-chapter-num",
|
||||
new object[] {result.ChapterNumber})
|
||||
};
|
||||
}
|
||||
else
|
||||
|
@ -294,7 +294,8 @@ public class ProcessSeries : IProcessSeries
|
||||
var maxVolume = series.Volumes.Max(v => (int) Parser.Parser.MaxNumberFromRange(v.Name));
|
||||
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;
|
||||
}
|
||||
|
@ -225,13 +225,13 @@ public class Startup
|
||||
IDirectoryService directoryService, IUnitOfWork unitOfWork, IBackupService backupService, IImageService imageService)
|
||||
{
|
||||
|
||||
var logger = serviceProvider.GetRequiredService<ILogger<Program>>();
|
||||
// Apply Migrations
|
||||
try
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
// Apply all migrations on startup
|
||||
var logger = serviceProvider.GetRequiredService<ILogger<Program>>();
|
||||
var userManager = serviceProvider.GetRequiredService<UserManager<AppUser>>();
|
||||
var dataContext = serviceProvider.GetRequiredService<DataContext>();
|
||||
|
||||
@ -256,7 +256,6 @@ public class Startup
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var logger = serviceProvider.GetRequiredService<ILogger<Program>>();
|
||||
logger.LogCritical(ex, "An error occurred during migration");
|
||||
}
|
||||
|
||||
@ -377,7 +376,6 @@ public class Startup
|
||||
{
|
||||
try
|
||||
{
|
||||
var logger = serviceProvider.GetRequiredService<ILogger<Startup>>();
|
||||
logger.LogInformation("Kavita - v{Version}", BuildInfo.Version);
|
||||
}
|
||||
catch (Exception)
|
||||
@ -387,8 +385,7 @@ public class Startup
|
||||
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)
|
||||
|
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",
|
||||
"maximumWarning": "2kb",
|
||||
"maximumWarning": "4kb",
|
||||
"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",
|
||||
"bootstrap": "^5.3.2",
|
||||
"charts.css": "^1.1.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"luxon": "^3.4.3",
|
||||
"ng-circle-progress": "^1.7.1",
|
||||
"ng-lazyload-image": "^9.1.3",
|
||||
@ -59,6 +60,7 @@
|
||||
"@angular/cli": "^17.0.0",
|
||||
"@angular/compiler-cli": "^17.0.1",
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/luxon": "^3.3.4",
|
||||
"@types/node": "^20.9.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
||||
@ -4710,6 +4712,12 @@
|
||||
"@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": {
|
||||
"version": "7946.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz",
|
||||
@ -8719,6 +8727,11 @@
|
||||
"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": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
|
||||
|
@ -38,6 +38,7 @@
|
||||
"@tweenjs/tween.js": "^21.0.0",
|
||||
"bootstrap": "^5.3.2",
|
||||
"charts.css": "^1.1.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"luxon": "^3.4.3",
|
||||
"ng-circle-progress": "^1.7.1",
|
||||
"ng-lazyload-image": "^9.1.3",
|
||||
@ -64,6 +65,7 @@
|
||||
"@angular/cli": "^17.0.0",
|
||||
"@angular/compiler-cli": "^17.0.1",
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/luxon": "^3.3.4",
|
||||
"@types/node": "^20.9.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
||||
|
@ -11,7 +11,7 @@ export class SafeHtmlPipe implements PipeTransform {
|
||||
private readonly dom: DomSanitizer = inject(DomSanitizer);
|
||||
constructor() {}
|
||||
|
||||
transform(value: string): unknown {
|
||||
transform(value: string): string | null {
|
||||
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 { AuthGuard } from "../_guards/auth.guard";
|
||||
import { AllSeriesComponent } from "../all-series/_components/all-series/all-series.component";
|
||||
|
||||
|
||||
export const routes: Routes = [
|
||||
{path: '**', component: AllSeriesComponent, pathMatch: 'full', canActivate: [AuthGuard]},
|
||||
{
|
||||
path: '',
|
||||
component: AllSeriesComponent,
|
||||
runGuardsAndResolvers: 'always',
|
||||
canActivate: [AuthGuard],
|
||||
}
|
||||
{path: '', component: AllSeriesComponent, pathMatch: 'full'},
|
||||
];
|
||||
|
@ -1,16 +1,6 @@
|
||||
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";
|
||||
|
||||
export const routes: Routes = [
|
||||
{path: '**', component: AnnouncementsComponent, pathMatch: 'full', canActivate: [AuthGuard, AdminGuard]},
|
||||
{
|
||||
path: '',
|
||||
runGuardsAndResolvers: 'always',
|
||||
canActivate: [AuthGuard, AdminGuard],
|
||||
children: [
|
||||
{path: 'announcements', component: AnnouncementsComponent},
|
||||
]
|
||||
}
|
||||
{path: '', component: AnnouncementsComponent, pathMatch: 'full'},
|
||||
];
|
||||
|
@ -1,15 +1,6 @@
|
||||
import { Routes } from "@angular/router";
|
||||
import { AuthGuard } from "../_guards/auth.guard";
|
||||
import { BookmarksComponent } from "../bookmark/_components/bookmarks/bookmarks.component";
|
||||
|
||||
export const routes: Routes = [
|
||||
{path: '**', component: BookmarksComponent, pathMatch: 'full', canActivate: [AuthGuard]},
|
||||
{
|
||||
path: '',
|
||||
runGuardsAndResolvers: 'always',
|
||||
canActivate: [AuthGuard],
|
||||
children: [
|
||||
{path: 'bookmarks', component: BookmarksComponent},
|
||||
]
|
||||
}
|
||||
{path: '', component: BookmarksComponent, pathMatch: 'full'},
|
||||
];
|
||||
|
@ -1,17 +1,9 @@
|
||||
import { Routes } from '@angular/router';
|
||||
import { AuthGuard } from '../_guards/auth.guard';
|
||||
import { AllCollectionsComponent } from '../collections/_components/all-collections/all-collections.component';
|
||||
import { CollectionDetailComponent } from '../collections/_components/collection-detail/collection-detail.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
runGuardsAndResolvers: 'always',
|
||||
canActivate: [AuthGuard],
|
||||
children: [
|
||||
{path: '', component: AllCollectionsComponent, pathMatch: 'full'},
|
||||
{path: ':id', component: CollectionDetailComponent},
|
||||
]
|
||||
}
|
||||
{path: '', component: AllCollectionsComponent, pathMatch: 'full'},
|
||||
{path: ':id', component: CollectionDetailComponent},
|
||||
];
|
||||
|
||||
|
@ -1,13 +1,10 @@
|
||||
import { Routes } from '@angular/router';
|
||||
import { AuthGuard } from '../_guards/auth.guard';
|
||||
import { DashboardComponent } from '../dashboard/_components/dashboard.component';
|
||||
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
runGuardsAndResolvers: 'always',
|
||||
canActivate: [AuthGuard],
|
||||
component: DashboardComponent,
|
||||
}
|
||||
];
|
||||
|
@ -1,18 +1,9 @@
|
||||
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 { ReadingListsComponent } from "../reading-list/_components/reading-lists/reading-lists.component";
|
||||
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
runGuardsAndResolvers: 'always',
|
||||
canActivate: [AuthGuard],
|
||||
children: [
|
||||
{path: '', component: ReadingListsComponent, pathMatch: 'full'},
|
||||
{path: ':id', component: ReadingListDetailComponent, pathMatch: 'full'},
|
||||
]
|
||||
},
|
||||
{path: '**', component: ReadingListsComponent, pathMatch: 'full', canActivate: [AuthGuard]},
|
||||
{path: '', component: ReadingListsComponent, pathMatch: 'full'},
|
||||
{path: ':id', component: ReadingListDetailComponent, pathMatch: 'full'},
|
||||
];
|
||||
|
@ -1,15 +1,6 @@
|
||||
import { Routes } from '@angular/router';
|
||||
import { AuthGuard } from '../_guards/auth.guard';
|
||||
import { UserPreferencesComponent } from '../user-settings/user-preferences/user-preferences.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{path: '**', component: UserPreferencesComponent, pathMatch: 'full'},
|
||||
{
|
||||
path: '',
|
||||
runGuardsAndResolvers: 'always',
|
||||
canActivate: [AuthGuard],
|
||||
children: [
|
||||
{path: '', component: UserPreferencesComponent, pathMatch: 'full'},
|
||||
]
|
||||
}
|
||||
{path: '', component: UserPreferencesComponent, pathMatch: 'full'},
|
||||
];
|
||||
|
@ -1,15 +1,6 @@
|
||||
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';
|
||||
|
||||
export const routes: Routes = [
|
||||
{path: '**', component: WantToReadComponent, pathMatch: 'full'},
|
||||
{
|
||||
path: '',
|
||||
runGuardsAndResolvers: 'always',
|
||||
canActivate: [AuthGuard],
|
||||
children: [
|
||||
{path: '', component: WantToReadComponent, pathMatch: 'full'},
|
||||
]
|
||||
}
|
||||
{path: '', component: WantToReadComponent, pathMatch: 'full'},
|
||||
];
|
||||
|
@ -1,5 +1,5 @@
|
||||
<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="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>
|
||||
|
@ -32,10 +32,6 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.no-images img {
|
||||
display: none;
|
||||
}
|
||||
|
@ -2,6 +2,5 @@
|
||||
background-color: var(--review-spoiler-bg-color);
|
||||
color: var(--review-spoiler-text-color);
|
||||
cursor: pointer;
|
||||
|
||||
}
|
||||
|
||||
|
@ -48,7 +48,7 @@
|
||||
<td><i class="fa-solid fa-arrow-turn-up" aria-hidden="true"></i></td>
|
||||
<td>...</td>
|
||||
</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 id="folder--{{idx}}">
|
||||
{{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 {
|
||||
|
||||
title: string = translate('all-series.title');
|
||||
title!: string;
|
||||
series: Series[] = [];
|
||||
loadingSeries = false;
|
||||
pagination: Pagination = new Pagination();
|
||||
@ -128,6 +128,7 @@ export class AllSeriesComponent implements OnInit {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.title = translate('all-series.title');
|
||||
this.hubService.messages$.pipe(debounceTime(6000), takeUntilDestroyed(this.destroyRef)).subscribe((event: Message<any>) => {
|
||||
if (event.event !== EVENTS.SeriesAdded) return;
|
||||
this.loadPage();
|
||||
@ -166,7 +167,11 @@ export class AllSeriesComponent implements OnInit {
|
||||
loadPage() {
|
||||
this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterActiveCheck);
|
||||
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.seriesService.getAllSeriesV2(undefined, undefined, this.filter!).pipe(take(1)).subscribe(series => {
|
||||
this.series = series.result;
|
||||
|
@ -6,22 +6,82 @@ import { AdminGuard } from './_guards/admin.guard';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: 'admin',
|
||||
canActivate: [AdminGuard],
|
||||
loadChildren: () => import('./_routes/admin-routing.module').then(m => m.routes)
|
||||
},
|
||||
{
|
||||
path: 'preferences',
|
||||
path: '',
|
||||
canActivate: [AuthGuard],
|
||||
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)
|
||||
runGuardsAndResolvers: 'always',
|
||||
children: [
|
||||
{
|
||||
path: 'admin',
|
||||
canActivate: [AdminGuard],
|
||||
loadChildren: () => import('./_routes/admin-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',
|
||||
@ -31,55 +91,6 @@ const routes: Routes = [
|
||||
path: 'login',
|
||||
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: 'libraries', pathMatch: 'full', redirectTo: 'home'},
|
||||
{path: '**', pathMatch: 'prefix', redirectTo: 'home'},
|
||||
|
@ -1,5 +1,5 @@
|
||||
<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'">
|
||||
<div class="fixed-top" #stickyTop>
|
||||
<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>
|
||||
<div class="col-1" (click)="goToPage(0)">{{pageNum}}</div>
|
||||
<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 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>
|
||||
|
@ -200,10 +200,6 @@ $action-bar-height: 38px;
|
||||
}
|
||||
}
|
||||
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.book-content {
|
||||
position: relative;
|
||||
margin: 0 0;
|
||||
@ -398,6 +394,7 @@ $pagination-opacity: 0;
|
||||
|
||||
|
||||
|
||||
|
||||
.highlight {
|
||||
background-color: rgba(65, 225, 100, 0.5) !important;
|
||||
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-secondary {
|
||||
color: var(--br-actionbar-button-text-color);
|
||||
|
@ -1614,13 +1614,12 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
// Responsibile for handling pagination only
|
||||
// Responsible for handling pagination only
|
||||
handleContainerClick(event: MouseEvent) {
|
||||
|
||||
//if (event.target)
|
||||
console.log('target: ', event.target);
|
||||
if (['action-bar'].some(className => (event.target as Element).classList.contains(className))) {
|
||||
console.log('exiting early')
|
||||
if (this.actionBarVisible || ['action-bar'].some(className => (event.target as Element).classList.contains(className))) {
|
||||
//console.log('exiting early')
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,3 @@
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.clickable:hover, .clickable:focus {
|
||||
background-color: lightgreen;
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
<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)">
|
||||
<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 *ngIf="total === 0 && !suppressArchiveWarning">
|
||||
<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>
|
||||
</div>
|
||||
<div class="card-overlay"></div>
|
||||
<div class="overlay-information {{centerOverlay ? 'overlay-information--centered' : ''}}" *ngIf="overlayInformation !== '' || overlayInformation !== undefined">
|
||||
<div class="position-relative">
|
||||
<span class="card-title library mx-auto" style="width: auto;" [ngbTooltip]="overlayInformation" placement="top">{{overlayInformation}}</span>
|
||||
<ng-container *ngIf="overlayInformation | safeHtml as info">
|
||||
<div class="overlay-information {{centerOverlay ? 'overlay-information--centered' : ''}}" *ngIf="info !== '' || info !== undefined">
|
||||
<div class="position-relative">
|
||||
<span class="card-title library mx-auto" style="width: auto;" [ngbTooltip]="info" placement="top" [innerHTML]="info"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="card-body" *ngIf="title.length > 0 || actions.length > 0">
|
||||
@ -54,7 +57,7 @@
|
||||
</ng-container>
|
||||
{{title}}
|
||||
</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>
|
||||
</span>
|
||||
</div>
|
||||
|
@ -17,39 +17,11 @@ $image-width: 160px;
|
||||
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 {
|
||||
outline: 2px solid var(--primary-color);
|
||||
}
|
||||
|
||||
|
||||
.img-top {
|
||||
height: $image-height;
|
||||
}
|
||||
|
||||
.progress-banner {
|
||||
width: $image-width;
|
||||
height: 5px;
|
||||
@ -78,7 +50,6 @@ $image-width: 160px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 158px;
|
||||
|
||||
}
|
||||
|
||||
.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 {
|
||||
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;
|
||||
@ -167,29 +106,8 @@ $image-width: 160px;
|
||||
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 {
|
||||
font-size: 13px;
|
||||
text-decoration: none;
|
||||
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,
|
||||
HostListener,
|
||||
inject,
|
||||
Input, NgZone,
|
||||
Input,
|
||||
OnInit,
|
||||
Output
|
||||
} from '@angular/core';
|
||||
@ -39,11 +39,11 @@ import {MangaFormatIconPipe} from "../../_pipes/manga-format-icon.pipe";
|
||||
import {SentenceCasePipe} from "../../_pipes/sentence-case.pipe";
|
||||
import {CommonModule} from "@angular/common";
|
||||
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 {NextExpectedChapter} from "../../_models/series-detail/next-expected-chapter";
|
||||
import {UtcToLocalTimePipe} from "../../_pipes/utc-to-local-time.pipe";
|
||||
import {TimeAgoPipe} from "../../_pipes/time-ago.pipe";
|
||||
import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe";
|
||||
|
||||
@Component({
|
||||
selector: 'app-card-item',
|
||||
@ -60,7 +60,8 @@ import {TimeAgoPipe} from "../../_pipes/time-ago.pipe";
|
||||
CardActionablesComponent,
|
||||
SentenceCasePipe,
|
||||
RouterLink,
|
||||
TranslocoModule
|
||||
TranslocoModule,
|
||||
SafeHtmlPipe
|
||||
],
|
||||
templateUrl: './card-item.component.html',
|
||||
styleUrls: ['./card-item.component.scss'],
|
||||
@ -68,6 +69,19 @@ import {TimeAgoPipe} from "../../_pipes/time-ago.pipe";
|
||||
})
|
||||
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
|
||||
*/
|
||||
@ -109,7 +123,7 @@ export class CardItemComponent implements OnInit {
|
||||
*/
|
||||
@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;
|
||||
/**
|
||||
@ -159,21 +173,6 @@ export class CardItemComponent implements OnInit {
|
||||
selectionInProgress: boolean = false;
|
||||
|
||||
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 {
|
||||
|
||||
@ -224,12 +223,15 @@ export class CardItemComponent implements OnInit {
|
||||
this.imageUrl = '';
|
||||
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;
|
||||
|
||||
if (nextDate.expectedDate) {
|
||||
const utcPipe = new UtcToLocalTimePipe();
|
||||
this.title = utcPipe.transform(nextDate.expectedDate, 'shortDate');
|
||||
this.title = '~ ' + utcPipe.transform(nextDate.expectedDate, 'shortDate');
|
||||
}
|
||||
|
||||
this.cdRef.markForCheck();
|
||||
|
@ -52,7 +52,7 @@
|
||||
</form>
|
||||
|
||||
<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()"
|
||||
[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>
|
||||
@ -62,7 +62,7 @@
|
||||
</ng-container>
|
||||
|
||||
</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)"
|
||||
[ngClass]="{'selected': !showApplyButton && selectedIndex === idx}">
|
||||
<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;
|
||||
}
|
||||
|
||||
.image-card {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selected {
|
||||
outline: 5px solid var(--primary-color);
|
||||
@ -38,4 +35,4 @@ ngx-file-drop ::ng-deep > div {
|
||||
display: inline-block;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
<ng-container *transloco="let t; read: 'external-series-card'">
|
||||
<ng-container *ngIf="data !== undefined">
|
||||
<div class="card">
|
||||
<div class="card-item-container card clickable">
|
||||
<div class="overlay" (click)="handleClick()">
|
||||
<ng-container>
|
||||
<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 {RouterLinkActive} from "@angular/router";
|
||||
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 {TranslocoDirective} from "@ngneat/transloco";
|
||||
import {SeriesPreviewDrawerComponent} from "../../_single-module/series-preview-drawer/series-preview-drawer.component";
|
||||
import {SeriesService} from "../../_services/series.service";
|
||||
|
||||
@Component({
|
||||
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) {
|
||||
this.formGroup.get('filterValue')?.patchValue('');
|
||||
this.formGroup.get('comparison')?.patchValue(StringComparisons[0]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@ -329,7 +330,10 @@ export class MetadataFilterRowComponent implements OnInit {
|
||||
}
|
||||
this.validComparisons$.next(comps);
|
||||
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;
|
||||
}
|
||||
|
||||
@ -339,6 +343,7 @@ export class MetadataFilterRowComponent implements OnInit {
|
||||
|
||||
if (this.loaded) {
|
||||
this.formGroup.get('filterValue')?.patchValue(false);
|
||||
this.formGroup.get('comparison')?.patchValue(DateComparisons[0]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@ -349,6 +354,7 @@ export class MetadataFilterRowComponent implements OnInit {
|
||||
|
||||
if (this.loaded) {
|
||||
this.formGroup.get('filterValue')?.patchValue(false);
|
||||
this.formGroup.get('comparison')?.patchValue(BooleanComparisons[0]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@ -363,7 +369,10 @@ export class MetadataFilterRowComponent implements OnInit {
|
||||
}
|
||||
this.validComparisons$.next(comps);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -163,6 +163,7 @@
|
||||
<div ngbDropdownMenu>
|
||||
<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="/all-filters/">{{t('all-filters')}}</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 (click)="logout()">{{t('logout')}}</a>
|
||||
|
@ -288,13 +288,15 @@
|
||||
<ng-container *ngIf="nextExpectedChapter">
|
||||
<ng-container [ngSwitch]="tabId">
|
||||
<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 *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 *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>
|
||||
|
@ -93,6 +93,7 @@ import {
|
||||
} from "../../../_single-module/series-preview-drawer/series-preview-drawer.component";
|
||||
import {PublicationStatus} from "../../../_models/metadata/publication-status";
|
||||
import {NextExpectedChapter} from "../../../_models/series-detail/next-expected-chapter";
|
||||
import {NextExpectedCardComponent} from "../../../cards/next-expected-card/next-expected-card.component";
|
||||
|
||||
interface RelatedSeriesPair {
|
||||
series: Series;
|
||||
@ -120,7 +121,7 @@ interface StoryLineItem {
|
||||
styleUrls: ['./series-detail.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
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 {
|
||||
|
||||
|
@ -21,6 +21,7 @@ import { AccountService } from 'src/app/_services/account.service';
|
||||
import { BytesPipe } from 'src/app/_pipes/bytes.pipe';
|
||||
import {translate} from "@ngneat/transloco";
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import {SAVER, Saver} from "../../_providers/saver.provider";
|
||||
|
||||
export const DEBOUNCE_TIME = 100;
|
||||
|
||||
@ -66,9 +67,11 @@ export class DownloadService {
|
||||
public activeDownloads$ = this.downloadsSource.asObservable();
|
||||
|
||||
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,
|
||||
private accountService: AccountService) { }
|
||||
constructor(@Inject(SAVER) private save: Saver) { }
|
||||
|
||||
|
||||
/**
|
||||
@ -269,22 +272,4 @@ export class DownloadService {
|
||||
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>
|
||||
<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}}
|
||||
<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>
|
||||
|
@ -1,7 +1,3 @@
|
||||
.list-group-item.no-click {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.list-group-item {
|
||||
cursor: pointer;
|
||||
}
|
@ -530,6 +530,11 @@
|
||||
"series-count": "{{common.series-count}}"
|
||||
},
|
||||
|
||||
"all-filters": {
|
||||
"title": "All Smart Filters",
|
||||
"count": "{{count}} {{customize-dashboard-modal.title-smart-filters}}"
|
||||
},
|
||||
|
||||
"announcements": {
|
||||
"title": "Announcements"
|
||||
},
|
||||
@ -1411,7 +1416,8 @@
|
||||
"settings": "Settings",
|
||||
"help": "Help",
|
||||
"announcements": "Announcements",
|
||||
"logout": "Logout"
|
||||
"logout": "Logout",
|
||||
"all-filters": "Smart Filters"
|
||||
},
|
||||
|
||||
"add-to-list-modal": {
|
||||
|
@ -27,6 +27,7 @@ import {switchMap} from "rxjs";
|
||||
import {provideTranslocoLocale} from "@ngneat/transloco-locale";
|
||||
import {provideTranslocoPersistTranslations} from "@ngneat/transloco-persist-translations";
|
||||
import {LazyLoadImageModule} from "ng-lazyload-image";
|
||||
import {getSaver, SAVER} from "./app/_providers/saver.provider";
|
||||
|
||||
const disableAnimations = !('animate' in document.documentElement);
|
||||
|
||||
@ -146,6 +147,7 @@ bootstrapApplication(AppComponent, {
|
||||
{ provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true },
|
||||
preLoad,
|
||||
Title,
|
||||
{ provide: SAVER, useFactory: getSaver },
|
||||
provideHttpClient(withInterceptorsFromDi())
|
||||
]
|
||||
} as ApplicationConfig)
|
||||
|
@ -12,4 +12,81 @@
|
||||
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()
|
||||
{
|
||||
ProgressStart 'Building UI'
|
||||
cd ../Kavita-webui/ || exit
|
||||
npm install
|
||||
npm run prod
|
||||
cd ../Kavita/ || exit
|
||||
ProgressEnd 'Building UI'
|
||||
|
||||
ProgressStart 'Building UI'
|
||||
echo 'Removing old wwwroot'
|
||||
rm -rf API/wwwroot/*
|
||||
cd ../Kavita-webui/ || exit
|
||||
cd UI/Web/ || exit
|
||||
echo 'Installing web dependencies'
|
||||
npm install
|
||||
npm install --legacy-peer-deps
|
||||
echo 'Building UI'
|
||||
npm run prod
|
||||
echo 'Copying back to Kavita wwwroot'
|
||||
cp -r dist/browser/* ../Kavita/API/wwwroot
|
||||
cd ../Kavita/ || exit
|
||||
mkdir -p ../../API/wwwroot
|
||||
cp -R dist/browser/* ../../API/wwwroot
|
||||
cd ../../ || exit
|
||||
ProgressEnd 'Building UI'
|
||||
}
|
||||
|
||||
|
0
identifier.sqlite
Normal file
0
identifier.sqlite
Normal file
@ -7,7 +7,7 @@
|
||||
"name": "GPL-3.0",
|
||||
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
||||
},
|
||||
"version": "0.7.10.8"
|
||||
"version": "0.7.10.9"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
|
Loading…
x
Reference in New Issue
Block a user