mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-24 00:52:23 -04:00
Filtering Bugs + OPDS Want To Read (#2210)
* Fixed Summary not allowing an empty field, as it should allow that. * Cleaned up some localization wording and put a todo for a bug with library filtering not working. * Added Want to Read to OPDS stream * Implemented the ability to disable adding filter rows for bookmarks page which only supports one filter type. * Fixed the library filtering code * Fixed a bunch of titles across the app. Fixed about system page not showing data quick enough. * Hide API key by default and show a button to unhide. Fixed a styling issue with input group buttons. * Fixed a hack to support zh_Hans language code to work for things like pt-br as well. * Fixed transloco not supporting same language scheme as Weblate, but somehow needs all languages. * Fixed the rating on series detail not being inline with other sections
This commit is contained in:
parent
f472745ae4
commit
59c7ef5aa5
@ -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<FeedLink>()
|
||||
{
|
||||
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<IActionResult> 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<IActionResult> GetCollections(string apiKey)
|
||||
|
@ -939,7 +939,6 @@ public class SeriesRepository : ISeriesRepository
|
||||
|
||||
private async Task<IQueryable<Series>> CreateFilteredSearchQueryableV2(int userId, FilterV2Dto filter, QueryContext queryContext, IQueryable<Series>? 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<int>();
|
||||
|
||||
|
||||
// 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<Series> ApplyLibraryFilter(FilterV2Dto filter, IQueryable<Series> query)
|
||||
{
|
||||
var filterIncludeLibs = new List<int>();
|
||||
var filterExcludeLibs = new List<int>();
|
||||
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<Series> BuildFilterQuery(int userId, FilterV2Dto filterDto, IQueryable<Series> query)
|
||||
|
@ -483,7 +483,7 @@ public static class SeriesFilter
|
||||
public static IQueryable<Series> HasSummary(this IQueryable<Series> queryable, bool condition,
|
||||
FilterComparison comparison, string queryString)
|
||||
{
|
||||
if (string.IsNullOrEmpty(queryString) || !condition) return queryable;
|
||||
if (!condition) return queryable;
|
||||
|
||||
switch (comparison)
|
||||
{
|
||||
|
@ -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",
|
||||
|
@ -42,7 +42,7 @@
|
||||
<p>{{t('setup-user-description')}}
|
||||
</p>
|
||||
<a class="email-link" href="{{emailLink}}" target="_blank" rel="noopener noreferrer">{{t('setup-user-account')}}</a>
|
||||
<app-api-key [title]="t('invite-url-label')" [tooltipText]="t('setup-user-account-tooltip')" [showRefresh]="false" [transform]="makeLink"></app-api-key>
|
||||
<app-api-key [title]="t('invite-url-label')" [tooltipText]="t('setup-user-account-tooltip')" [hideData]="false" [showRefresh]="false" [transform]="makeLink"></app-api-key>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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});
|
||||
}
|
||||
|
||||
|
@ -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, '<br>');
|
||||
// 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();
|
||||
});
|
||||
}
|
||||
|
@ -12,7 +12,7 @@
|
||||
</div>
|
||||
|
||||
<div class="col-md-2">
|
||||
<button class="btn btn-icon" (click)="addFilter()" [ngbTooltip]="t('add-rule')">
|
||||
<button class="btn btn-icon" (click)="addFilter()" [ngbTooltip]="t('add-rule')" [disabled]="statementLimit === -1 || (statementLimit > 0 && filter.statements.length >= statementLimit)">
|
||||
<i class="fa fa-solid fa-plus" aria-hidden="true"></i>
|
||||
<span class="visually-hidden" aria-hidden="true">{{t('add-rule')}}</span>
|
||||
</button>
|
||||
@ -58,9 +58,9 @@
|
||||
<div class="col-md-12">
|
||||
<app-metadata-row-filter [preset]="filterStmt" [availableFields]="availableFilterFields" (filterStatement)="updateFilter(i, $event)">
|
||||
<div class="col-md-1 ms-2 col-1">
|
||||
<button class="btn btn-icon" #removeBtn [ngbTooltip]="t('remove-rule', {num: i})" (click)="removeFilter(i)" *ngIf="i < (filter.statements.length - 1) && filter.statements.length > 1">
|
||||
<button class="btn btn-icon" #removeBtn [ngbTooltip]="t('remove-rule')" (click)="removeFilter(i)" *ngIf="i < (filter.statements.length - 1) && filter.statements.length > 1">
|
||||
<i class="fa-solid fa-minus" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{t('remove-rule', {num: i})}}</span>
|
||||
<span class="visually-hidden">{{t('remove-rule')}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</app-metadata-row-filter>
|
||||
|
@ -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<SeriesFilterV2> = new EventEmitter<SeriesFilterV2>();
|
||||
|
||||
|
@ -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 => {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -21,7 +21,7 @@
|
||||
<ng-template #filterSection>
|
||||
<div class="filter-section mx-auto pb-3" *ngIf="fullyLoaded">
|
||||
<div class="row justify-content-center g-0">
|
||||
<app-metadata-builder [filter]="filterV2!" [availableFilterFields]="allFilterFields" (update)="handleFilters($event)"></app-metadata-builder>
|
||||
<app-metadata-builder [filter]="filterV2!" [availableFilterFields]="allFilterFields" (update)="handleFilters($event)" [statementLimit]="filterSettings.statementLimit"></app-metadata-builder>
|
||||
</div>
|
||||
<form [formGroup]="sortGroup" class="container-fluid">
|
||||
<div class="row mb-3">
|
||||
|
@ -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) {
|
||||
|
@ -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');
|
||||
|
@ -1,7 +1,7 @@
|
||||
<div class="row">
|
||||
<div class="row g-0">
|
||||
<div class="col-auto custom-col clickable" [ngbPopover]="popContent"
|
||||
popoverTitle="Your Rating + Overall" popoverClass="md-popover">
|
||||
<span class="badge rounded-pill me-1">
|
||||
<span class="badge rounded-pill ps-0 me-1">
|
||||
<img class="me-1" ngSrc="assets/images/logo-32.png" width="24" height="24" alt="">
|
||||
<ng-container *ngIf="hasUserRated; else notYetRated">{{userRating * 20}}</ng-container>
|
||||
<ng-template #notYetRated>N/A</ng-template>
|
||||
|
@ -3,11 +3,16 @@
|
||||
<label for="api-key--{{title}}" class="form-label">{{title}}</label><span *ngIf="tooltipText.length > 0"> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="tooltip" role="button" tabindex="0"></i></span>
|
||||
<ng-template #tooltip>{{tooltipText}}</ng-template>
|
||||
<div class="input-group">
|
||||
<input #apiKey type="text" readonly class="form-control" id="api-key--{{title}}" aria-describedby="button-addon4" [value]="key" (click)="selectAll()">
|
||||
<div id="button-addon4">
|
||||
<button class="btn btn-outline-secondary" type="button" (click)="copy()" [title]="t('copy')"><span class="visually-hidden">Copy</span><i class="fa fa-copy" aria-hidden="true"></i></button>
|
||||
<button class="btn btn-danger" type="button" [ngbTooltip]="tipContent" (click)="refresh()" *ngIf="showRefresh"><span class="visually-hidden">Regenerate</span><i class="fa fa-sync-alt" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
<input #apiKey [type]="InputType" readonly class="form-control" id="api-key--{{title}}" aria-describedby="button-addon4" [value]="key" (click)="selectAll()">
|
||||
<button class="btn btn-outline-secondary" type="button" (click)="show()" [title]="t('show')" *ngIf="hideData">
|
||||
<span class="visually-hidden">t('show')</span><i class="fa fa-eye" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" type="button" (click)="copy()" [title]="t('copy')">
|
||||
<span class="visually-hidden">Copy</span><i class="fa fa-copy" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button class="btn btn-danger" type="button" [ngbTooltip]="tipContent" (click)="refresh()" *ngIf="showRefresh">
|
||||
<span class="visually-hidden">Regenerate</span><i class="fa fa-sync-alt" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ng-template #tipContent>
|
||||
{{t('regen-warning')}}
|
||||
</ng-template>
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -62,7 +62,7 @@
|
||||
<h4>{{t('email-updated-title')}}</h4>
|
||||
<p>{{t('email-updated-description')}}</p>
|
||||
<a class="email-link" href="{{emailLink}}" target="_blank" rel="noopener noreferrer">{{t('setup-user-account')}}</a>
|
||||
<app-api-key [title]="t('invite-url-label')" [tooltipText]="t('invite-url-tooltip')" [showRefresh]="false" [transform]="makeLink"></app-api-key>
|
||||
<app-api-key [title]="t('invite-url-label')" [tooltipText]="t('invite-url-tooltip')" [hideData]="false" [showRefresh]="false" [transform]="makeLink"></app-api-key>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #noPermission>
|
||||
|
@ -416,8 +416,8 @@
|
||||
<ng-container *ngIf="tab.fragment === FragmentID.Clients">
|
||||
<div class="alert alert-warning" role="alert" *ngIf="!opdsEnabled">{{t('clients-opds-alert')}}</div>
|
||||
<p>{{t('clients-opds-description')}}</p>
|
||||
<app-api-key [tooltipText]="t('clients-api-key-tooltip')"></app-api-key>
|
||||
<app-api-key [title]="t('clients-opds-url-tooltip')" [showRefresh]="false" [transform]="makeUrl"></app-api-key>
|
||||
<app-api-key [tooltipText]="t('clients-api-key-tooltip')" [hideData]="true"></app-api-key>
|
||||
<app-api-key [title]="t('clients-opds-url-tooltip')" [hideData]="true" [showRefresh]="false" [transform]="makeUrl"></app-api-key>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="tab.fragment === FragmentID.Theme">
|
||||
<app-theme-manager></app-theme-manager>
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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": {
|
||||
|
@ -54,6 +54,24 @@ export const preLoad = {
|
||||
deps: [AccountService, TranslocoService]
|
||||
};
|
||||
|
||||
function transformLanguageCodes(arr: Array<string>) {
|
||||
const transformedArray: Array<string> = [];
|
||||
|
||||
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',
|
||||
|
33
openapi.json
33
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": [
|
||||
|
Loading…
x
Reference in New Issue
Block a user