diff --git a/API/Controllers/OPDSController.cs b/API/Controllers/OPDSController.cs index 0f0b2880e..79380a4ea 100644 --- a/API/Controllers/OPDSController.cs +++ b/API/Controllers/OPDSController.cs @@ -142,6 +142,19 @@ public class OpdsController : BaseApiController } }); feed.Entries.Add(new FeedEntry() + { + Id = "wantToRead", + Title = await _localizationService.Translate(userId, "want-to-read"), + Content = new FeedEntryContent() + { + Text = await _localizationService.Translate(userId, "browse-want-to-read") + }, + Links = new List() + { + CreateLink(FeedLinkRelation.SubSection, FeedLinkType.AtomNavigation, $"{prefix}{apiKey}/want-to-read"), + } + }); + feed.Entries.Add(new FeedEntry() { Id = "allLibraries", Title = await _localizationService.Translate(userId, "libraries"), @@ -213,6 +226,27 @@ public class OpdsController : BaseApiController return CreateXmlResult(SerializeXml(feed)); } + [HttpGet("{apiKey}/want-to-read")] + [Produces("application/xml")] + public async Task GetWantToRead(string apiKey, [FromQuery] int pageNumber = 0) + { + var userId = await GetUser(apiKey); + if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableOpds) + return BadRequest(await _localizationService.Translate(userId, "opds-disabled")); + var (baseUrl, prefix) = await GetPrefix(); + var wantToReadSeries = await _unitOfWork.SeriesRepository.GetWantToReadForUserV2Async(userId, GetUserParams(pageNumber), _filterV2Dto); + var seriesMetadatas = await _unitOfWork.SeriesRepository.GetSeriesMetadataForIds(wantToReadSeries.Select(s => s.Id)); + + var feed = CreateFeed(await _localizationService.Translate(userId, "want-to-read"), $"{apiKey}/want-to-read", apiKey, prefix); + SetFeedId(feed, $"want-to-read"); + AddPagination(feed, wantToReadSeries, $"{prefix}{apiKey}/want-to-read"); + + feed.Entries.AddRange(wantToReadSeries.Select(seriesDto => + CreateSeries(seriesDto, seriesMetadatas.First(s => s.SeriesId == seriesDto.Id), apiKey, prefix, baseUrl))); + + return CreateXmlResult(SerializeXml(feed)); + } + [HttpGet("{apiKey}/collections")] [Produces("application/xml")] public async Task GetCollections(string apiKey) diff --git a/API/Data/Repositories/SeriesRepository.cs b/API/Data/Repositories/SeriesRepository.cs index b261ec270..2dd3fa972 100644 --- a/API/Data/Repositories/SeriesRepository.cs +++ b/API/Data/Repositories/SeriesRepository.cs @@ -939,7 +939,6 @@ public class SeriesRepository : ISeriesRepository private async Task> CreateFilteredSearchQueryableV2(int userId, FilterV2Dto filter, QueryContext queryContext, IQueryable? query = null) { - // NOTE: Why do we even have libraryId when the filter has the actual libraryIds? var userLibraries = await GetUserLibrariesForFilteredQuery(0, userId, queryContext); var userRating = await _context.AppUser.GetUserAgeRestriction(userId); var onlyParentSeries = await _context.AppUserPreferences.Where(u => u.AppUserId == userId) @@ -949,39 +948,66 @@ public class SeriesRepository : ISeriesRepository query ??= _context.Series .AsNoTracking(); - var filterLibs = new List(); + // First setup any FilterField.Libraries in the statements, as these don't have any traditional query statements applied here + query = ApplyLibraryFilter(filter, query); + + query = BuildFilterQuery(userId, filter, query); + + + query = query + .WhereIf(userLibraries.Count > 0, s => userLibraries.Contains(s.LibraryId)) + .WhereIf(onlyParentSeries, s => + s.RelationOf.Count == 0 || + s.RelationOf.All(p => p.RelationKind == RelationKind.Prequel)) + .RestrictAgainstAgeRestriction(userRating); + + + return ApplyLimit(query + .Sort(filter.SortOptions) + .AsSplitQuery(), filter.LimitTo); + } + + private static IQueryable ApplyLibraryFilter(FilterV2Dto filter, IQueryable query) + { + var filterIncludeLibs = new List(); + var filterExcludeLibs = new List(); if (filter.Statements != null) { foreach (var stmt in filter.Statements.Where(stmt => stmt.Field == FilterField.Libraries)) { - filterLibs.Add(int.Parse(stmt.Value)); + if (stmt.Comparison is FilterComparison.Equal or FilterComparison.Contains) + { + filterIncludeLibs.Add(int.Parse(stmt.Value)); + } + else + { + filterExcludeLibs.Add(int.Parse(stmt.Value)); + } } // Remove as filterLibs now has everything filter.Statements = filter.Statements.Where(stmt => stmt.Field != FilterField.Libraries).ToList(); } + // We now have a list of libraries the user wants it restricted to and libraries the user doesn't want in the list + // We need to check what the filer combo is to see how to next approach - query = BuildFilterQuery(userId, filter, query); - - query = query - .WhereIf(userLibraries.Count > 0, s => userLibraries.Contains(s.LibraryId)) - .WhereIf(filterLibs.Count > 0, s => filterLibs.Contains(s.LibraryId)) - .WhereIf(onlyParentSeries, s => - s.RelationOf.Count == 0 || - s.RelationOf.All(p => p.RelationKind == RelationKind.Prequel)); - - if (userRating.AgeRating != AgeRating.NotApplicable) + if (filter.Combination == FilterCombination.And) { - // this if statement is included in the extension - query = query.RestrictAgainstAgeRestriction(userRating); + // If the filter combo is AND, then we need 2 different queries + query = query + .WhereIf(filterIncludeLibs.Count > 0, s => filterIncludeLibs.Contains(s.LibraryId)) + .WhereIf(filterExcludeLibs.Count > 0, s => !filterExcludeLibs.Contains(s.LibraryId)); + } + else + { + // This is an OR statement. In that case we can just remove the filterExcludes + query = query.WhereIf(filterIncludeLibs.Count > 0, s => filterIncludeLibs.Contains(s.LibraryId)); } - return ApplyLimit(query - .Sort(filter.SortOptions) - .AsSplitQuery(), filter.LimitTo); + return query; } private static IQueryable BuildFilterQuery(int userId, FilterV2Dto filterDto, IQueryable query) diff --git a/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs b/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs index 33a733d2a..b69c7594e 100644 --- a/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs +++ b/API/Extensions/QueryExtensions/Filtering/SeriesFilter.cs @@ -483,7 +483,7 @@ public static class SeriesFilter public static IQueryable HasSummary(this IQueryable queryable, bool condition, FilterComparison comparison, string queryString) { - if (string.IsNullOrEmpty(queryString) || !condition) return queryable; + if (!condition) return queryable; switch (comparison) { diff --git a/API/I18N/en.json b/API/I18N/en.json index 29babf0bb..f4807c456 100644 --- a/API/I18N/en.json +++ b/API/I18N/en.json @@ -140,6 +140,8 @@ "on-deck": "On Deck", "browse-on-deck": "Browse On Deck", "recently-added": "Recently Added", + "want-to-read": "Want to Read", + "browse-want-to-read": "Browse Want to Read", "browse-recently-added": "Browse Recently Added", "reading-lists": "Reading Lists", "browse-reading-lists": "Browse by Reading Lists", diff --git a/UI/Web/src/app/admin/invite-user/invite-user.component.html b/UI/Web/src/app/admin/invite-user/invite-user.component.html index 7a9220bfa..124b91571 100644 --- a/UI/Web/src/app/admin/invite-user/invite-user.component.html +++ b/UI/Web/src/app/admin/invite-user/invite-user.component.html @@ -42,7 +42,7 @@

{{t('setup-user-description')}}

- + diff --git a/UI/Web/src/app/admin/manage-system/manage-system.component.ts b/UI/Web/src/app/admin/manage-system/manage-system.component.ts index db02a9fb4..4e6467ce9 100644 --- a/UI/Web/src/app/admin/manage-system/manage-system.component.ts +++ b/UI/Web/src/app/admin/manage-system/manage-system.component.ts @@ -1,67 +1,30 @@ -import { Component, OnInit } from '@angular/core'; -import { FormControl, FormGroup, Validators } from '@angular/forms'; -import { ToastrService } from 'ngx-toastr'; -import { take } from 'rxjs/operators'; -import { ServerService } from 'src/app/_services/server.service'; -import { SettingsService } from '../settings.service'; +import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnInit} from '@angular/core'; +import {ServerService} from 'src/app/_services/server.service'; import {ServerInfoSlim} from '../_models/server-info'; -import { ServerSettings } from '../_models/server-settings'; -import { NgIf } from '@angular/common'; -import {translate, TranslocoDirective} from "@ngneat/transloco"; +import {NgIf} from '@angular/common'; +import {TranslocoDirective} from "@ngneat/transloco"; @Component({ selector: 'app-manage-system', templateUrl: './manage-system.component.html', styleUrls: ['./manage-system.component.scss'], standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgIf, TranslocoDirective] }) export class ManageSystemComponent implements OnInit { - settingsForm: FormGroup = new FormGroup({}); - serverSettings!: ServerSettings; serverInfo!: ServerInfoSlim; + private readonly cdRef = inject(ChangeDetectorRef); - constructor(private settingsService: SettingsService, private toastr: ToastrService, - private serverService: ServerService) { } + constructor(public serverService: ServerService) { } ngOnInit(): void { - this.serverService.getServerInfo().pipe(take(1)).subscribe(info => { + this.serverService.getServerInfo().subscribe(info => { this.serverInfo = info; - }); - - this.settingsService.getServerSettings().pipe(take(1)).subscribe((settings: ServerSettings) => { - this.serverSettings = settings; - this.settingsForm.addControl('cacheDirectory', new FormControl(this.serverSettings.cacheDirectory, [Validators.required])); - this.settingsForm.addControl('taskScan', new FormControl(this.serverSettings.taskScan, [Validators.required])); - this.settingsForm.addControl('taskBackup', new FormControl(this.serverSettings.taskBackup, [Validators.required])); - this.settingsForm.addControl('port', new FormControl(this.serverSettings.port, [Validators.required])); - this.settingsForm.addControl('loggingLevel', new FormControl(this.serverSettings.loggingLevel, [Validators.required])); - this.settingsForm.addControl('allowStatCollection', new FormControl(this.serverSettings.allowStatCollection, [Validators.required])); - }); - } - - resetForm() { - this.settingsForm.get('cacheDirectory')?.setValue(this.serverSettings.cacheDirectory); - this.settingsForm.get('scanTask')?.setValue(this.serverSettings.taskScan); - this.settingsForm.get('taskBackup')?.setValue(this.serverSettings.taskBackup); - this.settingsForm.get('port')?.setValue(this.serverSettings.port); - this.settingsForm.get('loggingLevel')?.setValue(this.serverSettings.loggingLevel); - this.settingsForm.get('allowStatCollection')?.setValue(this.serverSettings.allowStatCollection); - this.settingsForm.markAsPristine(); - } - - saveSettings() { - const modelSettings = this.settingsForm.value; - - this.settingsService.updateServerSettings(modelSettings).pipe(take(1)).subscribe((settings: ServerSettings) => { - this.serverSettings = settings; - this.resetForm(); - this.toastr.success(translate('toasts.server-settings-updated')); - }, (err: any) => { - console.error('error: ', err); + this.cdRef.markForCheck(); }); } } diff --git a/UI/Web/src/app/bookmark/_components/bookmarks/bookmarks.component.ts b/UI/Web/src/app/bookmark/_components/bookmarks/bookmarks.component.ts index 07194d97c..a90a4a055 100644 --- a/UI/Web/src/app/bookmark/_components/bookmarks/bookmarks.component.ts +++ b/UI/Web/src/app/bookmark/_components/bookmarks/bookmarks.component.ts @@ -31,9 +31,10 @@ import { CardItemComponent } from '../../../cards/card-item/card-item.component' import { CardDetailLayoutComponent } from '../../../cards/card-detail-layout/card-detail-layout.component'; import { BulkOperationsComponent } from '../../../cards/bulk-operations/bulk-operations.component'; import { SideNavCompanionBarComponent } from '../../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component'; -import {TranslocoDirective, TranslocoService} from "@ngneat/transloco"; +import {translate, TranslocoDirective, TranslocoService} from "@ngneat/transloco"; import {FilterField} from "../../../_models/metadata/v2/filter-field"; import {SeriesFilterV2} from "../../../_models/metadata/v2/series-filter-v2"; +import {Title} from "@angular/platform-browser"; @Component({ selector: 'app-bookmarks', @@ -72,7 +73,7 @@ export class BookmarksComponent implements OnInit { public imageService: ImageService, private actionFactoryService: ActionFactoryService, private router: Router, private readonly cdRef: ChangeDetectorRef, private filterUtilityService: FilterUtilitiesService, private route: ActivatedRoute, - private jumpbarService: JumpbarService) { + private jumpbarService: JumpbarService, private titleService: Title) { this.filter = this.filterUtilityService.filterPresetsFromUrlV2(this.route.snapshot); if (this.filter.statements.length === 0) { this.filter!.statements.push(this.filterUtilityService.createSeriesV2DefaultStatement()); @@ -80,7 +81,8 @@ export class BookmarksComponent implements OnInit { this.filterActiveCheck = this.filterUtilityService.createSeriesV2Filter(); this.filterActiveCheck!.statements.push(this.filterUtilityService.createSeriesV2DefaultStatement()); this.filterSettings.presetsV2 = this.filter; - + this.filterSettings.statementLimit = 1; + this.titleService.setTitle('Kavita - ' + translate('bookmarks.title')); } ngOnInit(): void { diff --git a/UI/Web/src/app/cards/series-info-cards/series-info-cards.component.ts b/UI/Web/src/app/cards/series-info-cards/series-info-cards.component.ts index e89b4c4ca..bb8a15453 100644 --- a/UI/Web/src/app/cards/series-info-cards/series-info-cards.component.ts +++ b/UI/Web/src/app/cards/series-info-cards/series-info-cards.component.ts @@ -1,7 +1,8 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, - Component, DestroyRef, + Component, + DestroyRef, EventEmitter, inject, Input, @@ -10,15 +11,15 @@ import { Output } from '@angular/core'; import {debounceTime, filter, map} from 'rxjs'; -import { UtilityService } from 'src/app/shared/_services/utility.service'; -import { UserProgressUpdateEvent } from 'src/app/_models/events/user-progress-update-event'; -import { HourEstimateRange } from 'src/app/_models/series-detail/hour-estimate-range'; -import { MangaFormat } from 'src/app/_models/manga-format'; -import { Series } from 'src/app/_models/series'; -import { SeriesMetadata } from 'src/app/_models/metadata/series-metadata'; -import { AccountService } from 'src/app/_services/account.service'; -import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service'; -import { ReaderService } from 'src/app/_services/reader.service'; +import {UtilityService} from 'src/app/shared/_services/utility.service'; +import {UserProgressUpdateEvent} from 'src/app/_models/events/user-progress-update-event'; +import {HourEstimateRange} from 'src/app/_models/series-detail/hour-estimate-range'; +import {MangaFormat} from 'src/app/_models/manga-format'; +import {Series} from 'src/app/_models/series'; +import {SeriesMetadata} from 'src/app/_models/metadata/series-metadata'; +import {AccountService} from 'src/app/_services/account.service'; +import {EVENTS, MessageHubService} from 'src/app/_services/message-hub.service'; +import {ReaderService} from 'src/app/_services/reader.service'; import {FilterField} from "../../_models/metadata/v2/filter-field"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import {ScrobblingService} from "../../_services/scrobbling.service"; @@ -111,7 +112,8 @@ export class SeriesInfoCardsComponent implements OnInit, OnChanges { handleGoTo(queryParamName: FilterField, filter: any) { - if (filter + '' === '') return; + // Ignore the default case added as this query combo would never be valid + if (filter + '' === '' && queryParamName === FilterField.SeriesName) return; this.goTo.emit({queryParamName, filter}); } diff --git a/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.ts b/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.ts index 0fa787ad6..10a1224ff 100644 --- a/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.ts +++ b/UI/Web/src/app/collections/_components/collection-detail/collection-detail.component.ts @@ -229,10 +229,10 @@ export class CollectionDetailComponent implements OnInit, AfterContentChecked { this.collectionTag = matchingTags[0]; this.summary = (this.collectionTag.summary === null ? '' : this.collectionTag.summary).replace(/\n/g, '
'); - // TODO: This can be changed now that we have app-image and collection cover merge code + // TODO: This can be changed now that we have app-image and collection cover merge code (can it? Because we still have the case where there is no image) + // I can always tweak merge to allow blank slots and if just one item, just show that item image this.tagImage = this.imageService.randomize(this.imageService.getCollectionCoverImage(this.collectionTag.id)); - this.titleService.setTitle(this.translocoService.translate('errors.collection-invalid-access', {collectionName: this.collectionTag.title})); - // TODO: BUG: This title key is incorrect! + this.titleService.setTitle(this.translocoService.translate('collection-detail.title-alt', {collectionName: this.collectionTag.title})); this.cdRef.markForCheck(); }); } diff --git a/UI/Web/src/app/metadata-filter/_components/metadata-builder/metadata-builder.component.html b/UI/Web/src/app/metadata-filter/_components/metadata-builder/metadata-builder.component.html index 10312635c..3b1520f16 100644 --- a/UI/Web/src/app/metadata-filter/_components/metadata-builder/metadata-builder.component.html +++ b/UI/Web/src/app/metadata-filter/_components/metadata-builder/metadata-builder.component.html @@ -12,7 +12,7 @@
- @@ -58,9 +58,9 @@
-
diff --git a/UI/Web/src/app/metadata-filter/_components/metadata-builder/metadata-builder.component.ts b/UI/Web/src/app/metadata-filter/_components/metadata-builder/metadata-builder.component.ts index 5eeafeb0f..f25c9392d 100644 --- a/UI/Web/src/app/metadata-filter/_components/metadata-builder/metadata-builder.component.ts +++ b/UI/Web/src/app/metadata-filter/_components/metadata-builder/metadata-builder.component.ts @@ -46,6 +46,10 @@ import {translate, TranslocoDirective} from "@ngneat/transloco"; export class MetadataBuilderComponent implements OnInit { @Input({required: true}) filter!: SeriesFilterV2; + /** + * The number of statements that can be. 0 means unlimited. -1 means none. + */ + @Input() statementLimit = 0; @Input() availableFilterFields = allFields; @Output() update: EventEmitter = new EventEmitter(); 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 9f9a612b4..8af81a8af 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 @@ -171,7 +171,7 @@ export class MetadataFilterRowComponent implements OnInit { }))); case FilterField.Languages: return this.metadataService.getAllLanguages().pipe(map(statuses => statuses.map(status => { - return {value: status.isoCode, title: status.title + `(${status.isoCode})`} + return {value: status.isoCode, title: status.title + ` (${status.isoCode})`} }))); case FilterField.Formats: return of(mangaFormatFilters).pipe(map(statuses => statuses.map(status => { diff --git a/UI/Web/src/app/metadata-filter/filter-settings.ts b/UI/Web/src/app/metadata-filter/filter-settings.ts index 490c362c1..ce641b8c5 100644 --- a/UI/Web/src/app/metadata-filter/filter-settings.ts +++ b/UI/Web/src/app/metadata-filter/filter-settings.ts @@ -3,4 +3,8 @@ import { SeriesFilterV2 } from "../_models/metadata/v2/series-filter-v2"; export class FilterSettings { sortDisabled = false; presetsV2: SeriesFilterV2 | undefined; + /** + * The number of statements that can be on the filter. Set to 1 to disable adding more. + */ + statementLimit: number = 0; } diff --git a/UI/Web/src/app/metadata-filter/metadata-filter.component.html b/UI/Web/src/app/metadata-filter/metadata-filter.component.html index 65b49aa58..2a268ef9b 100644 --- a/UI/Web/src/app/metadata-filter/metadata-filter.component.html +++ b/UI/Web/src/app/metadata-filter/metadata-filter.component.html @@ -21,7 +21,7 @@
- +
diff --git a/UI/Web/src/app/pipe/compact-number.pipe.ts b/UI/Web/src/app/pipe/compact-number.pipe.ts index db5672078..f7f2d191a 100644 --- a/UI/Web/src/app/pipe/compact-number.pipe.ts +++ b/UI/Web/src/app/pipe/compact-number.pipe.ts @@ -13,8 +13,11 @@ export class CompactNumberPipe implements PipeTransform { transform(value: number): string { // Weblate allows some non-standard languages, like 'zh_Hans', which should be just 'zh'. So we handle that here - const locale = localStorage.getItem(AccountService.localeKey)?.split('_')[0]; - return this.transformValue(locale || 'en', value); + const key = localStorage.getItem(AccountService.localeKey)?.replace('_', '-'); + if (key?.endsWith('Hans')) { + return this.transformValue(key?.split('-')[0] || 'en', value); + } + return this.transformValue(key || 'en', value); } private transformValue(locale: string, value: number) { diff --git a/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.ts b/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.ts index 1e648bd72..a637e00df 100644 --- a/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.ts +++ b/UI/Web/src/app/reading-list/_components/reading-list-detail/reading-list-detail.component.ts @@ -38,6 +38,7 @@ import {FilterUtilitiesService} from "../../../shared/_services/filter-utilities import {FilterField} from "../../../_models/metadata/v2/filter-field"; import {FilterComparison} from "../../../_models/metadata/v2/filter-comparison"; import {MetadataDetailComponent} from "../../../series-detail/_components/metadata-detail/metadata-detail.component"; +import {Title} from "@angular/platform-browser"; @Component({ selector: 'app-reading-list-detail', @@ -76,7 +77,9 @@ export class ReadingListDetailComponent implements OnInit { private actionService: ActionService, private actionFactoryService: ActionFactoryService, public utilityService: UtilityService, public imageService: ImageService, private accountService: AccountService, private toastr: ToastrService, private confirmService: ConfirmService, private libraryService: LibraryService, private readerService: ReaderService, - private readonly cdRef: ChangeDetectorRef, private filterUtilityService: FilterUtilitiesService) {} + private readonly cdRef: ChangeDetectorRef, private filterUtilityService: FilterUtilitiesService, private titleService: Title) { + this.titleService.setTitle('Kavita - ' + translate('side-nav.reading-lists')); + } ngOnInit(): void { const listId = this.route.snapshot.paramMap.get('id'); diff --git a/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.html b/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.html index 4cf3663f8..e07ba4aff 100644 --- a/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.html +++ b/UI/Web/src/app/series-detail/_components/external-rating/external-rating.component.html @@ -1,7 +1,7 @@ -
+
- + {{userRating * 20}} N/A diff --git a/UI/Web/src/app/user-settings/api-key/api-key.component.html b/UI/Web/src/app/user-settings/api-key/api-key.component.html index b5052f0b3..377317390 100644 --- a/UI/Web/src/app/user-settings/api-key/api-key.component.html +++ b/UI/Web/src/app/user-settings/api-key/api-key.component.html @@ -3,11 +3,16 @@   {{tooltipText}}
- -
- - -
+ + + + {{t('regen-warning')}} diff --git a/UI/Web/src/app/user-settings/api-key/api-key.component.ts b/UI/Web/src/app/user-settings/api-key/api-key.component.ts index 7b85899b1..c2612447d 100644 --- a/UI/Web/src/app/user-settings/api-key/api-key.component.ts +++ b/UI/Web/src/app/user-settings/api-key/api-key.component.ts @@ -14,7 +14,7 @@ import {Clipboard} from '@angular/cdk/clipboard'; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; import { NgIf } from '@angular/common'; -import {TranslocoDirective, TranslocoService} from "@ngneat/transloco"; +import {translate, TranslocoDirective} from "@ngneat/transloco"; @Component({ selector: 'app-api-key', @@ -30,11 +30,14 @@ export class ApiKeyComponent implements OnInit { @Input() showRefresh: boolean = true; @Input() transform: (val: string) => string = (val: string) => val; @Input() tooltipText: string = ''; + @Input() hideData = true; @ViewChild('apiKey') inputElem!: ElementRef; key: string = ''; private readonly destroyRef = inject(DestroyRef); - private readonly translocoService = inject(TranslocoService); + get InputType() { + return this.hideData ? 'password' : 'text'; + } constructor(private confirmService: ConfirmService, private accountService: AccountService, private toastr: ToastrService, private clipboard: Clipboard, private readonly cdRef: ChangeDetectorRef) { } @@ -45,7 +48,7 @@ export class ApiKeyComponent implements OnInit { if (user) { key = user.apiKey; } else { - key = this.translocoService.translate('api-key.no-key'); + key = translate('api-key.no-key'); } if (this.transform != undefined) { @@ -63,13 +66,13 @@ export class ApiKeyComponent implements OnInit { } async refresh() { - if (!await this.confirmService.confirm(this.translocoService.translate('api-key.confirm-reset'))) { + if (!await this.confirmService.confirm(translate('api-key.confirm-reset'))) { return; } this.accountService.resetApiKey().subscribe(newKey => { this.key = newKey; this.cdRef.markForCheck(); - this.toastr.success(this.translocoService.translate('api-key.key-reset')); + this.toastr.success(translate('api-key.key-reset')); }); } @@ -80,4 +83,9 @@ export class ApiKeyComponent implements OnInit { } } + show() { + this.inputElem.nativeElement.setAttribute('type', 'text'); + this.cdRef.markForCheck(); + } + } diff --git a/UI/Web/src/app/user-settings/change-email/change-email.component.html b/UI/Web/src/app/user-settings/change-email/change-email.component.html index 57da94483..e736ad450 100644 --- a/UI/Web/src/app/user-settings/change-email/change-email.component.html +++ b/UI/Web/src/app/user-settings/change-email/change-email.component.html @@ -62,7 +62,7 @@

{{t('email-updated-title')}}

{{t('email-updated-description')}}

- + diff --git a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.html b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.html index 5ef2ff930..c3dee6f1c 100644 --- a/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.html +++ b/UI/Web/src/app/user-settings/user-preferences/user-preferences.component.html @@ -416,8 +416,8 @@

{{t('clients-opds-description')}}

- - + +
diff --git a/UI/Web/src/app/want-to-read/_components/want-to-read/want-to-read.component.ts b/UI/Web/src/app/want-to-read/_components/want-to-read/want-to-read.component.ts index 7f081a644..802fed61f 100644 --- a/UI/Web/src/app/want-to-read/_components/want-to-read/want-to-read.component.ts +++ b/UI/Web/src/app/want-to-read/_components/want-to-read/want-to-read.component.ts @@ -104,7 +104,7 @@ export class WantToReadComponent implements OnInit, AfterContentChecked { private readonly cdRef: ChangeDetectorRef, private scrollService: ScrollService, private hubService: MessageHubService, private jumpbarService: JumpbarService) { this.router.routeReuseStrategy.shouldReuseRoute = () => false; - this.titleService.setTitle(translate('want-to-read.title')); + this.titleService.setTitle('Kavita - ' + translate('want-to-read.title')); this.pagination = this.filterUtilityService.pagination(this.route.snapshot); diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json index 87c5c2728..ba5a5952c 100644 --- a/UI/Web/src/assets/langs/en.json +++ b/UI/Web/src/assets/langs/en.json @@ -267,6 +267,7 @@ "api-key": { "copy": "Copy", + "show": "Show", "regen-warning": "Regenerating your API key will invalidate any existing clients.", "no-key": "ERROR - KEY NOT SET", "confirm-reset": "This will invalidate any OPDS configurations you have setup. Are you sure you want to continue?", @@ -1694,7 +1695,7 @@ "or": "Match any of the following", "and": "Match all of the following", "add-rule": "Add Rule", - "remove-rule": "Remove Row {{num}}" + "remove-rule": "Remove Row" }, "filter-field-pipe": { diff --git a/UI/Web/src/main.ts b/UI/Web/src/main.ts index 3b14fd93a..69514615e 100644 --- a/UI/Web/src/main.ts +++ b/UI/Web/src/main.ts @@ -54,6 +54,24 @@ export const preLoad = { deps: [AccountService, TranslocoService] }; +function transformLanguageCodes(arr: Array) { + const transformedArray: Array = []; + + arr.forEach(code => { + // Add the original code + transformedArray.push(code); + + // Check if the code has a hyphen (like uk-UA) + if (code.includes('-')) { + // Transform hyphen to underscore and add to the array + const transformedCode = code.replace('-', '_'); + transformedArray.push(transformedCode); + } + }); + + return transformedArray; +} + // All Languages Kavita will support: http://www.lingoes.net/en/translator/langcode.htm const languageCodes = [ 'af', 'af-ZA', 'ar', 'ar-AE', 'ar-BH', 'ar-DZ', 'ar-EG', 'ar-IQ', 'ar-JO', 'ar-KW', @@ -78,13 +96,13 @@ const languageCodes = [ 'syr', 'syr-SY', 'ta', 'ta-IN', 'te', 'te-IN', 'th', 'th-TH', 'tl', 'tl-PH', 'tn', 'tn-ZA', 'tr', 'tr-TR', 'tt', 'tt-RU', 'ts', 'uk', 'uk-UA', 'ur', 'ur-PK', 'uz', 'uz-UZ', 'uz-UZ', 'vi', 'vi-VN', 'xh', 'xh-ZA', 'zh', 'zh-CN', 'zh-HK', 'zh-MO', - 'zh-SG', 'zh-TW', 'zu', 'zu-ZA', 'zh_Hans' + 'zh-SG', 'zh-TW', 'zu', 'zu-ZA', 'zh_Hans', ]; const translocoOptions = { config: { reRenderOnLangChange: true, - availableLangs: languageCodes, + availableLangs: transformLanguageCodes(languageCodes), prodMode: environment.production, defaultLang: 'en', fallbackLang: 'en', diff --git a/openapi.json b/openapi.json index f46950458..1cf089711 100644 --- a/openapi.json +++ b/openapi.json @@ -7,7 +7,7 @@ "name": "GPL-3.0", "url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE" }, - "version": "0.7.7.1" + "version": "0.7.7.7" }, "servers": [ { @@ -3422,6 +3422,37 @@ } } }, + "/api/Opds/{apiKey}/want-to-read": { + "get": { + "tags": [ + "Opds" + ], + "parameters": [ + { + "name": "apiKey", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "pageNumber", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + } + } + ], + "responses": { + "200": { + "description": "Success" + } + } + } + }, "/api/Opds/{apiKey}/collections": { "get": { "tags": [