diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 395c6edb6..99645263d 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -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 diff --git a/API.Tests/API.Tests.csproj b/API.Tests/API.Tests.csproj index a8b56591b..0f9c82178 100644 --- a/API.Tests/API.Tests.csproj +++ b/API.Tests/API.Tests.csproj @@ -7,11 +7,11 @@ - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/API.Tests/Helpers/SmartFilterHelperTests.cs b/API.Tests/Helpers/SmartFilterHelperTests.cs index 276bbf942..5508ab1a7 100644 --- a/API.Tests/Helpers/SmartFilterHelperTests.cs +++ b/API.Tests/Helpers/SmartFilterHelperTests.cs @@ -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() { diff --git a/API/API.csproj b/API/API.csproj index 29686bf30..8fec46517 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -94,13 +94,13 @@ - + - + diff --git a/API/Data/ManualMigrations/MigrateSmartFilterEncoding.cs b/API/Data/ManualMigrations/MigrateSmartFilterEncoding.cs index f3ac617e4..89b2d9cfc 100644 --- a/API/Data/ManualMigrations/MigrateSmartFilterEncoding.cs +++ b/API/Data/ManualMigrations/MigrateSmartFilterEncoding.cs @@ -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 diff --git a/API/I18N/en.json b/API/I18N/en.json index 861bebae1..e8235d3d5 100644 --- a/API/I18N/en.json +++ b/API/I18N/en.json @@ -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}", diff --git a/API/Services/SeriesService.cs b/API/Services/SeriesService.cs index 4bc3aec09..f944ed330 100644 --- a/API/Services/SeriesService.cs +++ b/API/Services/SeriesService.cs @@ -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 diff --git a/API/Services/Tasks/Scanner/ProcessSeries.cs b/API/Services/Tasks/Scanner/ProcessSeries.cs index 00246cfea..32492f22d 100644 --- a/API/Services/Tasks/Scanner/ProcessSeries.cs +++ b/API/Services/Tasks/Scanner/ProcessSeries.cs @@ -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; } diff --git a/API/Startup.cs b/API/Startup.cs index 2c9885280..95e2dba09 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -225,13 +225,13 @@ public class Startup IDirectoryService directoryService, IUnitOfWork unitOfWork, IBackupService backupService, IImageService imageService) { + var logger = serviceProvider.GetRequiredService>(); // Apply Migrations try { Task.Run(async () => { // Apply all migrations on startup - var logger = serviceProvider.GetRequiredService>(); var userManager = serviceProvider.GetRequiredService>(); var dataContext = serviceProvider.GetRequiredService(); @@ -256,7 +256,6 @@ public class Startup } catch (Exception ex) { - var logger = serviceProvider.GetRequiredService>(); logger.LogCritical(ex, "An error occurred during migration"); } @@ -377,7 +376,6 @@ public class Startup { try { - var logger = serviceProvider.GetRequiredService>(); 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>(); - _logger.LogInformation("Starting with base url as {BaseUrl}", basePath); + logger.LogInformation("Starting with base url as {BaseUrl}", basePath); } private static void UpdateBaseUrlInIndex(string baseUrl) diff --git a/API/config/config.7z b/API/config/config.7z new file mode 100644 index 000000000..7415012b2 Binary files /dev/null and b/API/config/config.7z differ diff --git a/Kavita.Email/Kavita.Email.csproj b/Kavita.Email/Kavita.Email.csproj deleted file mode 100644 index 5a9557890..000000000 --- a/Kavita.Email/Kavita.Email.csproj +++ /dev/null @@ -1,22 +0,0 @@ - - - - net6.0 - enable - enable - - - - - - - - - - - - - - - - diff --git a/UI/Web/angular.json b/UI/Web/angular.json index 5b4f25f13..41eb7a9c1 100644 --- a/UI/Web/angular.json +++ b/UI/Web/angular.json @@ -77,7 +77,7 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "2kb", + "maximumWarning": "4kb", "maximumError": "30kb" } ] diff --git a/UI/Web/package-lock.json b/UI/Web/package-lock.json index 9b235c769..44ca7b8d7 100644 --- a/UI/Web/package-lock.json +++ b/UI/Web/package-lock.json @@ -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", diff --git a/UI/Web/package.json b/UI/Web/package.json index a08d2211b..a8459dd1a 100644 --- a/UI/Web/package.json +++ b/UI/Web/package.json @@ -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", diff --git a/UI/Web/src/app/_pipes/safe-html.pipe.ts b/UI/Web/src/app/_pipes/safe-html.pipe.ts index 979197de4..0126ea1d1 100644 --- a/UI/Web/src/app/_pipes/safe-html.pipe.ts +++ b/UI/Web/src/app/_pipes/safe-html.pipe.ts @@ -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); } diff --git a/UI/Web/src/app/_providers/saver.provider.ts b/UI/Web/src/app/_providers/saver.provider.ts new file mode 100644 index 000000000..815fc55de --- /dev/null +++ b/UI/Web/src/app/_providers/saver.provider.ts @@ -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') + +export function getSaver(): Saver { + return saveAs; +} diff --git a/UI/Web/src/app/_routes/all-filters-routing.module.ts b/UI/Web/src/app/_routes/all-filters-routing.module.ts new file mode 100644 index 000000000..d9794d271 --- /dev/null +++ b/UI/Web/src/app/_routes/all-filters-routing.module.ts @@ -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'}, +]; diff --git a/UI/Web/src/app/_routes/all-series-routing.module.ts b/UI/Web/src/app/_routes/all-series-routing.module.ts index eafdb03f5..d9dfaaf96 100644 --- a/UI/Web/src/app/_routes/all-series-routing.module.ts +++ b/UI/Web/src/app/_routes/all-series-routing.module.ts @@ -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'}, ]; diff --git a/UI/Web/src/app/_routes/announcements-routing.module.ts b/UI/Web/src/app/_routes/announcements-routing.module.ts index c5715ed74..34c31b11a 100644 --- a/UI/Web/src/app/_routes/announcements-routing.module.ts +++ b/UI/Web/src/app/_routes/announcements-routing.module.ts @@ -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'}, ]; diff --git a/UI/Web/src/app/_routes/bookmark-routing.module.ts b/UI/Web/src/app/_routes/bookmark-routing.module.ts index d303173d8..6da971e08 100644 --- a/UI/Web/src/app/_routes/bookmark-routing.module.ts +++ b/UI/Web/src/app/_routes/bookmark-routing.module.ts @@ -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'}, ]; diff --git a/UI/Web/src/app/_routes/collections-routing.module.ts b/UI/Web/src/app/_routes/collections-routing.module.ts index 0227fda89..80510c8f6 100644 --- a/UI/Web/src/app/_routes/collections-routing.module.ts +++ b/UI/Web/src/app/_routes/collections-routing.module.ts @@ -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}, ]; diff --git a/UI/Web/src/app/_routes/dashboard-routing.module.ts b/UI/Web/src/app/_routes/dashboard-routing.module.ts index b08722fe0..e035a47ea 100644 --- a/UI/Web/src/app/_routes/dashboard-routing.module.ts +++ b/UI/Web/src/app/_routes/dashboard-routing.module.ts @@ -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, } ]; diff --git a/UI/Web/src/app/_routes/reading-list-routing.module.ts b/UI/Web/src/app/_routes/reading-list-routing.module.ts index e53e056c9..f1c8e1410 100644 --- a/UI/Web/src/app/_routes/reading-list-routing.module.ts +++ b/UI/Web/src/app/_routes/reading-list-routing.module.ts @@ -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'}, ]; diff --git a/UI/Web/src/app/_routes/user-settings-routing.module.ts b/UI/Web/src/app/_routes/user-settings-routing.module.ts index 4e02386ac..a099acec7 100644 --- a/UI/Web/src/app/_routes/user-settings-routing.module.ts +++ b/UI/Web/src/app/_routes/user-settings-routing.module.ts @@ -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'}, ]; diff --git a/UI/Web/src/app/_routes/want-to-read-routing.module.ts b/UI/Web/src/app/_routes/want-to-read-routing.module.ts index ad0d58b05..b3301d9f9 100644 --- a/UI/Web/src/app/_routes/want-to-read-routing.module.ts +++ b/UI/Web/src/app/_routes/want-to-read-routing.module.ts @@ -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'}, ]; diff --git a/UI/Web/src/app/_single-module/review-card/review-card.component.html b/UI/Web/src/app/_single-module/review-card/review-card.component.html index 41722fb89..3f8e562a9 100644 --- a/UI/Web/src/app/_single-module/review-card/review-card.component.html +++ b/UI/Web/src/app/_single-module/review-card/review-card.component.html @@ -1,5 +1,5 @@ -
+
diff --git a/UI/Web/src/app/_single-module/review-card/review-card.component.scss b/UI/Web/src/app/_single-module/review-card/review-card.component.scss index bb425ec08..e1caefb65 100644 --- a/UI/Web/src/app/_single-module/review-card/review-card.component.scss +++ b/UI/Web/src/app/_single-module/review-card/review-card.component.scss @@ -32,10 +32,6 @@ overflow: hidden; } -.card { - cursor: pointer; -} - .no-images img { display: none; } diff --git a/UI/Web/src/app/_single-module/spoiler/spoiler.component.scss b/UI/Web/src/app/_single-module/spoiler/spoiler.component.scss index e3559f7af..e2b2c98a9 100644 --- a/UI/Web/src/app/_single-module/spoiler/spoiler.component.scss +++ b/UI/Web/src/app/_single-module/spoiler/spoiler.component.scss @@ -2,6 +2,5 @@ background-color: var(--review-spoiler-bg-color); color: var(--review-spoiler-text-color); cursor: pointer; - } diff --git a/UI/Web/src/app/admin/_modals/directory-picker/directory-picker.component.html b/UI/Web/src/app/admin/_modals/directory-picker/directory-picker.component.html index 80fc94896..6cf59f901 100644 --- a/UI/Web/src/app/admin/_modals/directory-picker/directory-picker.component.html +++ b/UI/Web/src/app/admin/_modals/directory-picker/directory-picker.component.html @@ -48,7 +48,7 @@ ... - + {{folder.name}} diff --git a/UI/Web/src/app/all-filters/all-filters.component.html b/UI/Web/src/app/all-filters/all-filters.component.html new file mode 100644 index 000000000..97a237886 --- /dev/null +++ b/UI/Web/src/app/all-filters/all-filters.component.html @@ -0,0 +1,34 @@ + + +

+ {{t('title')}} +

+
{{t('count', {count: filters.length | number})}}
+
+ + + +
+
+
+
+
+ + + +
+
+
+
+ {{item.name}} +
+
+
+
+ +
diff --git a/UI/Web/src/app/all-filters/all-filters.component.scss b/UI/Web/src/app/all-filters/all-filters.component.scss new file mode 100644 index 000000000..9d517144c --- /dev/null +++ b/UI/Web/src/app/all-filters/all-filters.component.scss @@ -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; +} diff --git a/UI/Web/src/app/all-filters/all-filters.component.ts b/UI/Web/src/app/all-filters/all-filters.component.ts new file mode 100644 index 000000000..7863f59ee --- /dev/null +++ b/UI/Web/src/app/all-filters/all-filters.component.ts @@ -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 = []; + 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) => { + // 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('¦'); + } + +} diff --git a/UI/Web/src/app/all-series/_components/all-series/all-series.component.ts b/UI/Web/src/app/all-series/_components/all-series/all-series.component.ts index 65be00ba7..5b7fea779 100644 --- a/UI/Web/src/app/all-series/_components/all-series/all-series.component.ts +++ b/UI/Web/src/app/all-series/_components/all-series/all-series.component.ts @@ -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) => { 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('�')[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; diff --git a/UI/Web/src/app/app-routing.module.ts b/UI/Web/src/app/app-routing.module.ts index 20e1fcdb0..ea85a1c74 100644 --- a/UI/Web/src/app/app-routing.module.ts +++ b/UI/Web/src/app/app-routing.module.ts @@ -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'}, diff --git a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.html b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.html index 5caabfb4d..78a988091 100644 --- a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.html +++ b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.html @@ -1,5 +1,5 @@
+ tabindex="0" #reader (click)="handleContainerClick($event)" [ngClass]="{'clickable' : cursorIsPointer}">
{{t('skip-header')}} @@ -44,7 +44,7 @@
{{pageNum}}
- +
{{maxPages - 1}}
diff --git a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.scss b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.scss index 625af0449..7a0e0b783 100644 --- a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.scss +++ b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.scss @@ -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); diff --git a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts index df16b8260..a4f80e4a6 100644 --- a/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts +++ b/UI/Web/src/app/book-reader/_components/book-reader/book-reader.component.ts @@ -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; } diff --git a/UI/Web/src/app/cards/_modals/bulk-add-to-collection/bulk-add-to-collection.component.scss b/UI/Web/src/app/cards/_modals/bulk-add-to-collection/bulk-add-to-collection.component.scss index 29342b14b..03416cd05 100644 --- a/UI/Web/src/app/cards/_modals/bulk-add-to-collection/bulk-add-to-collection.component.scss +++ b/UI/Web/src/app/cards/_modals/bulk-add-to-collection/bulk-add-to-collection.component.scss @@ -1,7 +1,3 @@ -.clickable { - cursor: pointer; -} - .clickable:hover, .clickable:focus { background-color: lightgreen; } diff --git a/UI/Web/src/app/cards/card-item/card-item.component.html b/UI/Web/src/app/cards/card-item/card-item.component.html index c9b53787c..1dee096b6 100644 --- a/UI/Web/src/app/cards/card-item/card-item.component.html +++ b/UI/Web/src/app/cards/card-item/card-item.component.html @@ -1,8 +1,8 @@ -
+
- + @@ -34,11 +34,14 @@ {{count}}
-
-
- {{overlayInformation}} + +
+
+ +
-
+ +
@@ -54,7 +57,7 @@ {{title}} - +
diff --git a/UI/Web/src/app/cards/card-item/card-item.component.scss b/UI/Web/src/app/cards/card-item/card-item.component.scss index 794e93233..79e57fc46 100644 --- a/UI/Web/src/app/cards/card-item/card-item.component.scss +++ b/UI/Web/src/app/cards/card-item/card-item.component.scss @@ -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; -} diff --git a/UI/Web/src/app/cards/card-item/card-item.component.ts b/UI/Web/src/app/cards/card-item/card-item.component.ts index e809f384c..81e5356e2 100644 --- a/UI/Web/src/app/cards/card-item/card-item.component.ts +++ b/UI/Web/src/app/cards/card-item/card-item.component.ts @@ -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 = ` + +
${tokens[0]}
${tokens[1]}
`; 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(); diff --git a/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.html b/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.html index 9dad9ea6a..e4ad639e2 100644 --- a/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.html +++ b/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.html @@ -52,7 +52,7 @@
-
@@ -62,7 +62,7 @@
-
diff --git a/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.scss b/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.scss index d631bbdf5..485a22298 100644 --- a/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.scss +++ b/UI/Web/src/app/cards/cover-image-chooser/cover-image-chooser.component.scss @@ -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; } - } \ No newline at end of file + } diff --git a/UI/Web/src/app/cards/external-series-card/external-series-card.component.html b/UI/Web/src/app/cards/external-series-card/external-series-card.component.html index f0267a66a..feb6caf72 100644 --- a/UI/Web/src/app/cards/external-series-card/external-series-card.component.html +++ b/UI/Web/src/app/cards/external-series-card/external-series-card.component.html @@ -1,6 +1,6 @@ -
+
diff --git a/UI/Web/src/app/cards/external-series-card/external-series-card.component.scss b/UI/Web/src/app/cards/external-series-card/external-series-card.component.scss index a04dc0812..e69de29bb 100644 --- a/UI/Web/src/app/cards/external-series-card/external-series-card.component.scss +++ b/UI/Web/src/app/cards/external-series-card/external-series-card.component.scss @@ -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; -} diff --git a/UI/Web/src/app/cards/external-series-card/external-series-card.component.ts b/UI/Web/src/app/cards/external-series-card/external-series-card.component.ts index 8f8e5af17..c531d1550 100644 --- a/UI/Web/src/app/cards/external-series-card/external-series-card.component.ts +++ b/UI/Web/src/app/cards/external-series-card/external-series-card.component.ts @@ -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', diff --git a/UI/Web/src/app/cards/next-expected-card/next-expected-card.component.html b/UI/Web/src/app/cards/next-expected-card/next-expected-card.component.html new file mode 100644 index 000000000..75fe1ce82 --- /dev/null +++ b/UI/Web/src/app/cards/next-expected-card/next-expected-card.component.html @@ -0,0 +1,22 @@ +
+
+ + +
+ +
+
+ + + + +
+
+
+ +
+ +
+ {{title}} +
+
diff --git a/UI/Web/src/app/cards/next-expected-card/next-expected-card.component.scss b/UI/Web/src/app/cards/next-expected-card/next-expected-card.component.scss new file mode 100644 index 000000000..e558bb0a5 --- /dev/null +++ b/UI/Web/src/app/cards/next-expected-card/next-expected-card.component.scss @@ -0,0 +1,11 @@ +::ng-deep .extreme-blur { + filter: brightness(50%) blur(4px) +} + +.overlay-information { + background-color: transparent; +} + +.card-title { + width: 146px; +} diff --git a/UI/Web/src/app/cards/next-expected-card/next-expected-card.component.ts b/UI/Web/src/app/cards/next-expected-card/next-expected-card.component.ts new file mode 100644 index 000000000..8fc6a2efd --- /dev/null +++ b/UI/Web/src/app/cards/next-expected-card/next-expected-card.component.ts @@ -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 = `
${tokens[0]}
${tokens[1]}
`; + + if (this.entity.expectedDate) { + const utcPipe = new UtcToLocalTimePipe(); + this.title = '~ ' + utcPipe.transform(this.entity.expectedDate, 'shortDate'); + } + this.cdRef.markForCheck(); + } + +} diff --git a/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.ts b/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.ts index b78229772..9243466db 100644 --- a/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.ts +++ b/UI/Web/src/app/metadata-filter/_components/metadata-filter-row/metadata-filter-row.component.ts @@ -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; } } diff --git a/UI/Web/src/app/nav/_components/nav-header/nav-header.component.html b/UI/Web/src/app/nav/_components/nav-header/nav-header.component.html index 6c264a654..a271c3ee8 100644 --- a/UI/Web/src/app/nav/_components/nav-header/nav-header.component.html +++ b/UI/Web/src/app/nav/_components/nav-header/nav-header.component.html @@ -163,6 +163,7 @@
{{t('server-settings')}} {{t('settings')}} + {{t('all-filters')}} {{t('help')}} {{t('announcements')}} {{t('logout')}} diff --git a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html index 92403bcb0..0475bff10 100644 --- a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html +++ b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.html @@ -288,13 +288,15 @@ - + - + - + diff --git a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts index 5e2720227..9d01b60e1 100644 --- a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts +++ b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts @@ -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 { diff --git a/UI/Web/src/app/shared/_services/download.service.ts b/UI/Web/src/app/shared/_services/download.service.ts index 79ad52964..20c44046e 100644 --- a/UI/Web/src/app/shared/_services/download.service.ts +++ b/UI/Web/src/app/shared/_services/download.service.ts @@ -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); - } - } diff --git a/UI/Web/src/app/statistics/_components/_modals/generic-list-modal/generic-list-modal.component.html b/UI/Web/src/app/statistics/_components/_modals/generic-list-modal/generic-list-modal.component.html index 00fbf43e4..39591cca6 100644 --- a/UI/Web/src/app/statistics/_components/_modals/generic-list-modal/generic-list-modal.component.html +++ b/UI/Web/src/app/statistics/_components/_modals/generic-list-modal/generic-list-modal.component.html @@ -13,7 +13,7 @@
    -
  • +
  • {{item}}