Polish Part 3 (#2424)

This commit is contained in:
Joe Milazzo 2023-11-10 07:56:30 -06:00 committed by GitHub
parent a018d6828e
commit 944830ca73
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 518 additions and 493 deletions

View File

@ -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

View File

@ -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>

View File

@ -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()
{

View File

@ -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" />

View File

@ -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

View File

@ -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}",

View File

@ -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

View File

@ -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;
}

View File

@ -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

Binary file not shown.

View File

@ -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>

View File

@ -77,7 +77,7 @@
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumWarning": "4kb",
"maximumError": "30kb"
}
]

View File

@ -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",

View File

@ -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",

View File

@ -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);
}

View 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;
}

View 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'},
];

View File

@ -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'},
];

View File

@ -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'},
];

View File

@ -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'},
];

View File

@ -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},
];

View File

@ -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,
}
];

View File

@ -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'},
];

View File

@ -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'},
];

View File

@ -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'},
];

View File

@ -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>

View File

@ -32,10 +32,6 @@
overflow: hidden;
}
.card {
cursor: pointer;
}
.no-images img {
display: none;
}

View File

@ -2,6 +2,5 @@
background-color: var(--review-spoiler-bg-color);
color: var(--review-spoiler-text-color);
cursor: pointer;
}

View File

@ -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}}

View 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>

View 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;
}

View 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('¦');
}
}

View File

@ -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;

View File

@ -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'},

View File

@ -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>

View File

@ -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);

View File

@ -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;
}

View File

@ -1,7 +1,3 @@
.clickable {
cursor: pointer;
}
.clickable:hover, .clickable:focus {
background-color: lightgreen;
}

View File

@ -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>

View File

@ -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;
}

View File

@ -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();

View File

@ -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>

View File

@ -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;
}
}
}

View File

@ -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>

View File

@ -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;
}

View File

@ -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',

View File

@ -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>

View File

@ -0,0 +1,11 @@
::ng-deep .extreme-blur {
filter: brightness(50%) blur(4px)
}
.overlay-information {
background-color: transparent;
}
.card-title {
width: 146px;
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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 {

View File

@ -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);
}
}

View File

@ -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>

View File

@ -1,7 +1,3 @@
.list-group-item.no-click {
cursor: not-allowed;
}
.list-group-item {
cursor: pointer;
}

View File

@ -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": {

View File

@ -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)

View File

@ -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;
}
}

View File

@ -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
View File

View 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": [
{