Merge remote-tracking branch 'upstream/develop' into feature/qol-bulk-operations-style-changes
This commit is contained in:
commit
707fb2cc72
374 changed files with 9175 additions and 3420 deletions
|
|
@ -13,7 +13,7 @@
|
|||
}
|
||||
|
||||
.subtitle {
|
||||
color: lightgrey;
|
||||
color: var(--detail-subtitle-color);
|
||||
font-weight: bold;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ export enum LibraryType {
|
|||
Book = 2,
|
||||
Images = 3,
|
||||
LightNovel = 4,
|
||||
/**
|
||||
* Comic (Legacy)
|
||||
*/
|
||||
ComicVine = 5
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ export interface Person extends IHasCover {
|
|||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
aliases: Array<string>;
|
||||
coverImage?: string;
|
||||
coverImageLocked: boolean;
|
||||
malId?: number;
|
||||
|
|
|
|||
|
|
@ -7,12 +7,13 @@ import {Library} from '../_models/library/library';
|
|||
import {ReadingList} from '../_models/reading-list';
|
||||
import {Series} from '../_models/series';
|
||||
import {Volume} from '../_models/volume';
|
||||
import {AccountService} from './account.service';
|
||||
import {AccountService, Role} from './account.service';
|
||||
import {DeviceService} from './device.service';
|
||||
import {SideNavStream} from "../_models/sidenav/sidenav-stream";
|
||||
import {SmartFilter} from "../_models/metadata/v2/smart-filter";
|
||||
import {translate} from "@jsverse/transloco";
|
||||
import {Person} from "../_models/metadata/person";
|
||||
import {User} from '../_models/user';
|
||||
|
||||
export enum Action {
|
||||
Submenu = -1,
|
||||
|
|
@ -106,7 +107,7 @@ export enum Action {
|
|||
Promote = 24,
|
||||
UnPromote = 25,
|
||||
/**
|
||||
* Invoke a refresh covers as false to generate colorscapes
|
||||
* Invoke refresh covers as false to generate colorscapes
|
||||
*/
|
||||
GenerateColorScape = 26,
|
||||
/**
|
||||
|
|
@ -116,20 +117,31 @@ export enum Action {
|
|||
/**
|
||||
* Match an entity with an upstream system
|
||||
*/
|
||||
Match = 28
|
||||
Match = 28,
|
||||
/**
|
||||
* Merge two (or more?) entities
|
||||
*/
|
||||
Merge = 29,
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for an action
|
||||
*/
|
||||
export type ActionCallback<T> = (action: ActionItem<T>, data: T) => void;
|
||||
export type ActionAllowedCallback<T> = (action: ActionItem<T>) => boolean;
|
||||
export type ActionCallback<T> = (action: ActionItem<T>, entity: T) => void;
|
||||
export type ActionShouldRenderFunc<T> = (action: ActionItem<T>, entity: T, user: User) => boolean;
|
||||
|
||||
export interface ActionItem<T> {
|
||||
title: string;
|
||||
description: string;
|
||||
action: Action;
|
||||
callback: ActionCallback<T>;
|
||||
/**
|
||||
* Roles required to be present for ActionItem to show. If empty, assumes anyone can see. At least one needs to apply.
|
||||
*/
|
||||
requiredRoles: Role[];
|
||||
/**
|
||||
* @deprecated Use required Roles instead
|
||||
*/
|
||||
requiresAdmin: boolean;
|
||||
children: Array<ActionItem<T>>;
|
||||
/**
|
||||
|
|
@ -145,94 +157,98 @@ export interface ActionItem<T> {
|
|||
* Extra data that needs to be sent back from the card item. Used mainly for dynamicList. This will be the item from dyanamicList return
|
||||
*/
|
||||
_extra?: {title: string, data: any};
|
||||
/**
|
||||
* Will call on each action to determine if it should show for the appropriate entity based on state and user
|
||||
*/
|
||||
shouldRender: ActionShouldRenderFunc<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Entities that can be actioned upon
|
||||
*/
|
||||
export type ActionableEntity = Volume | Series | Chapter | ReadingList | UserCollection | Person | Library | SideNavStream | SmartFilter | null;
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ActionFactoryService {
|
||||
libraryActions: Array<ActionItem<Library>> = [];
|
||||
|
||||
seriesActions: Array<ActionItem<Series>> = [];
|
||||
|
||||
volumeActions: Array<ActionItem<Volume>> = [];
|
||||
|
||||
chapterActions: Array<ActionItem<Chapter>> = [];
|
||||
|
||||
collectionTagActions: Array<ActionItem<UserCollection>> = [];
|
||||
|
||||
readingListActions: Array<ActionItem<ReadingList>> = [];
|
||||
|
||||
bookmarkActions: Array<ActionItem<Series>> = [];
|
||||
|
||||
private libraryActions: Array<ActionItem<Library>> = [];
|
||||
private seriesActions: Array<ActionItem<Series>> = [];
|
||||
private volumeActions: Array<ActionItem<Volume>> = [];
|
||||
private chapterActions: Array<ActionItem<Chapter>> = [];
|
||||
private collectionTagActions: Array<ActionItem<UserCollection>> = [];
|
||||
private readingListActions: Array<ActionItem<ReadingList>> = [];
|
||||
private bookmarkActions: Array<ActionItem<Series>> = [];
|
||||
private personActions: Array<ActionItem<Person>> = [];
|
||||
|
||||
sideNavStreamActions: Array<ActionItem<SideNavStream>> = [];
|
||||
smartFilterActions: Array<ActionItem<SmartFilter>> = [];
|
||||
|
||||
sideNavHomeActions: Array<ActionItem<void>> = [];
|
||||
|
||||
isAdmin = false;
|
||||
|
||||
private sideNavStreamActions: Array<ActionItem<SideNavStream>> = [];
|
||||
private smartFilterActions: Array<ActionItem<SmartFilter>> = [];
|
||||
private sideNavHomeActions: Array<ActionItem<void>> = [];
|
||||
|
||||
constructor(private accountService: AccountService, private deviceService: DeviceService) {
|
||||
this.accountService.currentUser$.subscribe((user) => {
|
||||
if (user) {
|
||||
this.isAdmin = this.accountService.hasAdminRole(user);
|
||||
} else {
|
||||
this._resetActions();
|
||||
return; // If user is logged out, we don't need to do anything
|
||||
}
|
||||
|
||||
this.accountService.currentUser$.subscribe((_) => {
|
||||
this._resetActions();
|
||||
});
|
||||
}
|
||||
|
||||
getLibraryActions(callback: ActionCallback<Library>) {
|
||||
return this.applyCallbackToList(this.libraryActions, callback);
|
||||
getLibraryActions(callback: ActionCallback<Library>, shouldRenderFunc: ActionShouldRenderFunc<Library> = this.dummyShouldRender) {
|
||||
return this.applyCallbackToList(this.libraryActions, callback, shouldRenderFunc) as ActionItem<Library>[];
|
||||
}
|
||||
|
||||
getSeriesActions(callback: ActionCallback<Series>) {
|
||||
return this.applyCallbackToList(this.seriesActions, callback);
|
||||
getSeriesActions(callback: ActionCallback<Series>, shouldRenderFunc: ActionShouldRenderFunc<Series> = this.basicReadRender) {
|
||||
return this.applyCallbackToList(this.seriesActions, callback, shouldRenderFunc);
|
||||
}
|
||||
|
||||
getSideNavStreamActions(callback: ActionCallback<SideNavStream>) {
|
||||
return this.applyCallbackToList(this.sideNavStreamActions, callback);
|
||||
getSideNavStreamActions(callback: ActionCallback<SideNavStream>, shouldRenderFunc: ActionShouldRenderFunc<SideNavStream> = this.dummyShouldRender) {
|
||||
return this.applyCallbackToList(this.sideNavStreamActions, callback, shouldRenderFunc);
|
||||
}
|
||||
|
||||
getSmartFilterActions(callback: ActionCallback<SmartFilter>) {
|
||||
return this.applyCallbackToList(this.smartFilterActions, callback);
|
||||
getSmartFilterActions(callback: ActionCallback<SmartFilter>, shouldRenderFunc: ActionShouldRenderFunc<SmartFilter> = this.dummyShouldRender) {
|
||||
return this.applyCallbackToList(this.smartFilterActions, callback, shouldRenderFunc);
|
||||
}
|
||||
|
||||
getVolumeActions(callback: ActionCallback<Volume>) {
|
||||
return this.applyCallbackToList(this.volumeActions, callback);
|
||||
getVolumeActions(callback: ActionCallback<Volume>, shouldRenderFunc: ActionShouldRenderFunc<Volume> = this.basicReadRender) {
|
||||
return this.applyCallbackToList(this.volumeActions, callback, shouldRenderFunc);
|
||||
}
|
||||
|
||||
getChapterActions(callback: ActionCallback<Chapter>) {
|
||||
return this.applyCallbackToList(this.chapterActions, callback);
|
||||
getChapterActions(callback: ActionCallback<Chapter>, shouldRenderFunc: ActionShouldRenderFunc<Chapter> = this.basicReadRender) {
|
||||
return this.applyCallbackToList(this.chapterActions, callback, shouldRenderFunc);
|
||||
}
|
||||
|
||||
getCollectionTagActions(callback: ActionCallback<UserCollection>) {
|
||||
return this.applyCallbackToList(this.collectionTagActions, callback);
|
||||
getCollectionTagActions(callback: ActionCallback<UserCollection>, shouldRenderFunc: ActionShouldRenderFunc<UserCollection> = this.dummyShouldRender) {
|
||||
return this.applyCallbackToList(this.collectionTagActions, callback, shouldRenderFunc);
|
||||
}
|
||||
|
||||
getReadingListActions(callback: ActionCallback<ReadingList>) {
|
||||
return this.applyCallbackToList(this.readingListActions, callback);
|
||||
getReadingListActions(callback: ActionCallback<ReadingList>, shouldRenderFunc: ActionShouldRenderFunc<ReadingList> = this.dummyShouldRender) {
|
||||
return this.applyCallbackToList(this.readingListActions, callback, shouldRenderFunc);
|
||||
}
|
||||
|
||||
getBookmarkActions(callback: ActionCallback<Series>) {
|
||||
return this.applyCallbackToList(this.bookmarkActions, callback);
|
||||
getBookmarkActions(callback: ActionCallback<Series>, shouldRenderFunc: ActionShouldRenderFunc<Series> = this.dummyShouldRender) {
|
||||
return this.applyCallbackToList(this.bookmarkActions, callback, shouldRenderFunc);
|
||||
}
|
||||
|
||||
getPersonActions(callback: ActionCallback<Person>) {
|
||||
return this.applyCallbackToList(this.personActions, callback);
|
||||
getPersonActions(callback: ActionCallback<Person>, shouldRenderFunc: ActionShouldRenderFunc<Person> = this.dummyShouldRender) {
|
||||
return this.applyCallbackToList(this.personActions, callback, shouldRenderFunc);
|
||||
}
|
||||
|
||||
getSideNavHomeActions(callback: ActionCallback<void>) {
|
||||
return this.applyCallbackToList(this.sideNavHomeActions, callback);
|
||||
getSideNavHomeActions(callback: ActionCallback<void>, shouldRenderFunc: ActionShouldRenderFunc<void> = this.dummyShouldRender) {
|
||||
return this.applyCallbackToList(this.sideNavHomeActions, callback, shouldRenderFunc);
|
||||
}
|
||||
|
||||
dummyCallback(action: ActionItem<any>, data: any) {}
|
||||
dummyCallback(action: ActionItem<any>, entity: any) {}
|
||||
dummyShouldRender(action: ActionItem<any>, entity: any, user: User) {return true;}
|
||||
basicReadRender(action: ActionItem<any>, entity: any, user: User) {
|
||||
if (entity === null || entity === undefined) return true;
|
||||
if (!entity.hasOwnProperty('pagesRead') && !entity.hasOwnProperty('pages')) return true;
|
||||
|
||||
switch (action.action) {
|
||||
case(Action.MarkAsRead):
|
||||
return entity.pagesRead < entity.pages;
|
||||
case(Action.MarkAsUnread):
|
||||
return entity.pagesRead !== 0;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
filterSendToAction(actions: Array<ActionItem<Chapter>>, chapter: Chapter) {
|
||||
// if (chapter.files.filter(f => f.format === MangaFormat.EPUB || f.format === MangaFormat.PDF).length !== chapter.files.length) {
|
||||
|
|
@ -275,7 +291,7 @@ export class ActionFactoryService {
|
|||
return tasks.filter(t => !blacklist.includes(t.action));
|
||||
}
|
||||
|
||||
getBulkLibraryActions(callback: ActionCallback<Library>) {
|
||||
getBulkLibraryActions(callback: ActionCallback<Library>, shouldRenderFunc: ActionShouldRenderFunc<Library> = this.dummyShouldRender) {
|
||||
|
||||
// Scan is currently not supported due to the backend not being able to handle it yet
|
||||
const actions = this.flattenActions<Library>(this.libraryActions).filter(a => {
|
||||
|
|
@ -289,11 +305,13 @@ export class ActionFactoryService {
|
|||
dynamicList: undefined,
|
||||
action: Action.CopySettings,
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: shouldRenderFunc,
|
||||
children: [],
|
||||
requiredRoles: [Role.Admin],
|
||||
requiresAdmin: true,
|
||||
title: 'copy-settings'
|
||||
})
|
||||
return this.applyCallbackToList(actions, callback);
|
||||
return this.applyCallbackToList(actions, callback, shouldRenderFunc) as ActionItem<Library>[];
|
||||
}
|
||||
|
||||
flattenActions<T>(actions: Array<ActionItem<T>>): Array<ActionItem<T>> {
|
||||
|
|
@ -319,7 +337,9 @@ export class ActionFactoryService {
|
|||
title: 'scan-library',
|
||||
description: 'scan-library-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: true,
|
||||
requiredRoles: [Role.Admin],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -327,14 +347,18 @@ export class ActionFactoryService {
|
|||
title: 'others',
|
||||
description: '',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: true,
|
||||
requiredRoles: [Role.Admin],
|
||||
children: [
|
||||
{
|
||||
action: Action.RefreshMetadata,
|
||||
title: 'refresh-covers',
|
||||
description: 'refresh-covers-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: true,
|
||||
requiredRoles: [Role.Admin],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -342,7 +366,9 @@ export class ActionFactoryService {
|
|||
title: 'generate-colorscape',
|
||||
description: 'generate-colorscape-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: true,
|
||||
requiredRoles: [Role.Admin],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -350,7 +376,9 @@ export class ActionFactoryService {
|
|||
title: 'analyze-files',
|
||||
description: 'analyze-files-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: true,
|
||||
requiredRoles: [Role.Admin],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -358,7 +386,9 @@ export class ActionFactoryService {
|
|||
title: 'delete',
|
||||
description: 'delete-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: true,
|
||||
requiredRoles: [Role.Admin],
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
|
|
@ -368,7 +398,9 @@ export class ActionFactoryService {
|
|||
title: 'settings',
|
||||
description: 'settings-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: true,
|
||||
requiredRoles: [Role.Admin],
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
|
|
@ -379,7 +411,9 @@ export class ActionFactoryService {
|
|||
title: 'edit',
|
||||
description: 'edit-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -387,7 +421,9 @@ export class ActionFactoryService {
|
|||
title: 'delete',
|
||||
description: 'delete-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
class: 'danger',
|
||||
children: [],
|
||||
},
|
||||
|
|
@ -396,7 +432,9 @@ export class ActionFactoryService {
|
|||
title: 'promote',
|
||||
description: 'promote-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -404,7 +442,9 @@ export class ActionFactoryService {
|
|||
title: 'unpromote',
|
||||
description: 'unpromote-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
|
|
@ -415,7 +455,9 @@ export class ActionFactoryService {
|
|||
title: 'mark-as-read',
|
||||
description: 'mark-as-read-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -423,7 +465,9 @@ export class ActionFactoryService {
|
|||
title: 'mark-as-unread',
|
||||
description: 'mark-as-unread-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -431,7 +475,9 @@ export class ActionFactoryService {
|
|||
title: 'scan-series',
|
||||
description: 'scan-series-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: true,
|
||||
requiredRoles: [Role.Admin],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -439,14 +485,18 @@ export class ActionFactoryService {
|
|||
title: 'add-to',
|
||||
description: '',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [
|
||||
{
|
||||
action: Action.AddToWantToReadList,
|
||||
title: 'add-to-want-to-read',
|
||||
description: 'add-to-want-to-read-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -454,7 +504,9 @@ export class ActionFactoryService {
|
|||
title: 'remove-from-want-to-read',
|
||||
description: 'remove-to-want-to-read-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -462,7 +514,9 @@ export class ActionFactoryService {
|
|||
title: 'add-to-reading-list',
|
||||
description: 'add-to-reading-list-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -470,26 +524,11 @@ export class ActionFactoryService {
|
|||
title: 'add-to-collection',
|
||||
description: 'add-to-collection-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
|
||||
// {
|
||||
// action: Action.AddToScrobbleHold,
|
||||
// title: 'add-to-scrobble-hold',
|
||||
// description: 'add-to-scrobble-hold-tooltip',
|
||||
// callback: this.dummyCallback,
|
||||
// requiresAdmin: true,
|
||||
// children: [],
|
||||
// },
|
||||
// {
|
||||
// action: Action.RemoveFromScrobbleHold,
|
||||
// title: 'remove-from-scrobble-hold',
|
||||
// description: 'remove-from-scrobble-hold-tooltip',
|
||||
// callback: this.dummyCallback,
|
||||
// requiresAdmin: true,
|
||||
// children: [],
|
||||
// },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -497,14 +536,18 @@ export class ActionFactoryService {
|
|||
title: 'send-to',
|
||||
description: 'send-to-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [
|
||||
{
|
||||
action: Action.SendTo,
|
||||
title: '',
|
||||
description: '',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
dynamicList: this.deviceService.devices$.pipe(map((devices: Array<Device>) => devices.map(d => {
|
||||
return {'title': d.name, 'data': d};
|
||||
}), shareReplay())),
|
||||
|
|
@ -517,14 +560,18 @@ export class ActionFactoryService {
|
|||
title: 'others',
|
||||
description: '',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: true,
|
||||
requiredRoles: [],
|
||||
children: [
|
||||
{
|
||||
action: Action.RefreshMetadata,
|
||||
title: 'refresh-covers',
|
||||
description: 'refresh-covers-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: true,
|
||||
requiredRoles: [Role.Admin],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -532,7 +579,9 @@ export class ActionFactoryService {
|
|||
title: 'generate-colorscape',
|
||||
description: 'generate-colorscape-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: true,
|
||||
requiredRoles: [Role.Admin],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -540,7 +589,9 @@ export class ActionFactoryService {
|
|||
title: 'analyze-files',
|
||||
description: 'analyze-files-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: true,
|
||||
requiredRoles: [Role.Admin],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -548,7 +599,9 @@ export class ActionFactoryService {
|
|||
title: 'delete',
|
||||
description: 'delete-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: true,
|
||||
requiredRoles: [Role.Admin],
|
||||
class: 'danger',
|
||||
children: [],
|
||||
},
|
||||
|
|
@ -559,7 +612,9 @@ export class ActionFactoryService {
|
|||
title: 'match',
|
||||
description: 'match-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: true,
|
||||
requiredRoles: [Role.Admin],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -567,7 +622,9 @@ export class ActionFactoryService {
|
|||
title: 'download',
|
||||
description: 'download-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [Role.Download],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -575,7 +632,9 @@ export class ActionFactoryService {
|
|||
title: 'edit',
|
||||
description: 'edit-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: true,
|
||||
requiredRoles: [Role.Admin],
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
|
|
@ -586,7 +645,9 @@ export class ActionFactoryService {
|
|||
title: 'read-incognito',
|
||||
description: 'read-incognito-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -594,7 +655,9 @@ export class ActionFactoryService {
|
|||
title: 'mark-as-read',
|
||||
description: 'mark-as-read-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -602,7 +665,9 @@ export class ActionFactoryService {
|
|||
title: 'mark-as-unread',
|
||||
description: 'mark-as-unread-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -610,14 +675,18 @@ export class ActionFactoryService {
|
|||
title: 'add-to',
|
||||
description: '=',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [
|
||||
{
|
||||
action: Action.AddToReadingList,
|
||||
title: 'add-to-reading-list',
|
||||
description: 'add-to-reading-list-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
}
|
||||
]
|
||||
|
|
@ -627,14 +696,18 @@ export class ActionFactoryService {
|
|||
title: 'send-to',
|
||||
description: 'send-to-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [
|
||||
{
|
||||
action: Action.SendTo,
|
||||
title: '',
|
||||
description: '',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
dynamicList: this.deviceService.devices$.pipe(map((devices: Array<Device>) => devices.map(d => {
|
||||
return {'title': d.name, 'data': d};
|
||||
}), shareReplay())),
|
||||
|
|
@ -647,14 +720,18 @@ export class ActionFactoryService {
|
|||
title: 'others',
|
||||
description: '',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [
|
||||
{
|
||||
action: Action.Delete,
|
||||
title: 'delete',
|
||||
description: 'delete-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: true,
|
||||
requiredRoles: [Role.Admin],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -662,7 +739,9 @@ export class ActionFactoryService {
|
|||
title: 'download',
|
||||
description: 'download-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
]
|
||||
|
|
@ -672,7 +751,9 @@ export class ActionFactoryService {
|
|||
title: 'details',
|
||||
description: 'edit-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
|
|
@ -683,7 +764,9 @@ export class ActionFactoryService {
|
|||
title: 'read-incognito',
|
||||
description: 'read-incognito-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -691,7 +774,9 @@ export class ActionFactoryService {
|
|||
title: 'mark-as-read',
|
||||
description: 'mark-as-read-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -699,7 +784,9 @@ export class ActionFactoryService {
|
|||
title: 'mark-as-unread',
|
||||
description: 'mark-as-unread-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -707,14 +794,18 @@ export class ActionFactoryService {
|
|||
title: 'add-to',
|
||||
description: '',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [
|
||||
{
|
||||
action: Action.AddToReadingList,
|
||||
title: 'add-to-reading-list',
|
||||
description: 'add-to-reading-list-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
}
|
||||
]
|
||||
|
|
@ -724,14 +815,18 @@ export class ActionFactoryService {
|
|||
title: 'send-to',
|
||||
description: 'send-to-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [
|
||||
{
|
||||
action: Action.SendTo,
|
||||
title: '',
|
||||
description: '',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
dynamicList: this.deviceService.devices$.pipe(map((devices: Array<Device>) => devices.map(d => {
|
||||
return {'title': d.name, 'data': d};
|
||||
}), shareReplay())),
|
||||
|
|
@ -745,14 +840,18 @@ export class ActionFactoryService {
|
|||
title: 'others',
|
||||
description: '',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [
|
||||
{
|
||||
action: Action.Delete,
|
||||
title: 'delete',
|
||||
description: 'delete-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: true,
|
||||
requiredRoles: [Role.Admin],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -760,7 +859,9 @@ export class ActionFactoryService {
|
|||
title: 'download',
|
||||
description: 'download-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [Role.Download],
|
||||
children: [],
|
||||
},
|
||||
]
|
||||
|
|
@ -770,7 +871,9 @@ export class ActionFactoryService {
|
|||
title: 'edit',
|
||||
description: 'edit-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
|
|
@ -781,7 +884,9 @@ export class ActionFactoryService {
|
|||
title: 'edit',
|
||||
description: 'edit-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -789,7 +894,9 @@ export class ActionFactoryService {
|
|||
title: 'delete',
|
||||
description: 'delete-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
class: 'danger',
|
||||
children: [],
|
||||
},
|
||||
|
|
@ -798,7 +905,9 @@ export class ActionFactoryService {
|
|||
title: 'promote',
|
||||
description: 'promote-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -806,7 +915,9 @@ export class ActionFactoryService {
|
|||
title: 'unpromote',
|
||||
description: 'unpromote-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
|
|
@ -817,7 +928,19 @@ export class ActionFactoryService {
|
|||
title: 'edit',
|
||||
description: 'edit-person-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: true,
|
||||
requiredRoles: [Role.Admin],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
action: Action.Merge,
|
||||
title: 'merge',
|
||||
description: 'merge-person-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: true,
|
||||
requiredRoles: [Role.Admin],
|
||||
children: [],
|
||||
}
|
||||
];
|
||||
|
|
@ -828,7 +951,9 @@ export class ActionFactoryService {
|
|||
title: 'view-series',
|
||||
description: 'view-series-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -836,7 +961,9 @@ export class ActionFactoryService {
|
|||
title: 'download',
|
||||
description: 'download-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -844,8 +971,10 @@ export class ActionFactoryService {
|
|||
title: 'clear',
|
||||
description: 'delete-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
class: 'danger',
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
|
|
@ -856,7 +985,9 @@ export class ActionFactoryService {
|
|||
title: 'mark-visible',
|
||||
description: 'mark-visible-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -864,7 +995,9 @@ export class ActionFactoryService {
|
|||
title: 'mark-invisible',
|
||||
description: 'mark-invisible-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
|
|
@ -875,7 +1008,9 @@ export class ActionFactoryService {
|
|||
title: 'rename',
|
||||
description: 'rename-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -883,7 +1018,9 @@ export class ActionFactoryService {
|
|||
title: 'delete',
|
||||
description: 'delete-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
|
|
@ -894,7 +1031,9 @@ export class ActionFactoryService {
|
|||
title: 'reorder',
|
||||
description: '',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
}
|
||||
]
|
||||
|
|
@ -902,21 +1041,24 @@ export class ActionFactoryService {
|
|||
|
||||
}
|
||||
|
||||
private applyCallback(action: ActionItem<any>, callback: (action: ActionItem<any>, data: any) => void) {
|
||||
private applyCallback(action: ActionItem<any>, callback: ActionCallback<any>, shouldRenderFunc: ActionShouldRenderFunc<any>) {
|
||||
action.callback = callback;
|
||||
action.shouldRender = shouldRenderFunc;
|
||||
|
||||
if (action.children === null || action.children?.length === 0) return;
|
||||
|
||||
action.children?.forEach((childAction) => {
|
||||
this.applyCallback(childAction, callback);
|
||||
this.applyCallback(childAction, callback, shouldRenderFunc);
|
||||
});
|
||||
}
|
||||
|
||||
public applyCallbackToList(list: Array<ActionItem<any>>, callback: (action: ActionItem<any>, data: any) => void): Array<ActionItem<any>> {
|
||||
public applyCallbackToList(list: Array<ActionItem<any>>,
|
||||
callback: ActionCallback<any>,
|
||||
shouldRenderFunc: ActionShouldRenderFunc<any> = this.dummyShouldRender): Array<ActionItem<any>> {
|
||||
const actions = list.map((a) => {
|
||||
return { ...a };
|
||||
});
|
||||
actions.forEach((action) => this.applyCallback(action, callback));
|
||||
actions.forEach((action) => this.applyCallback(action, callback, shouldRenderFunc));
|
||||
return actions;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -473,8 +473,7 @@ export class ActionService {
|
|||
}
|
||||
|
||||
async deleteMultipleVolumes(volumes: Array<Volume>, callback?: BooleanActionCallback) {
|
||||
// TODO: Change translation key back to "toasts.confirm-delete-multiple-volumes"
|
||||
if (!await this.confirmService.confirm(translate('toasts.confirm-delete-multiple-chapters', {count: volumes.length}))) return;
|
||||
if (!await this.confirmService.confirm(translate('toasts.confirm-delete-multiple-volumes', {count: volumes.length}))) return;
|
||||
|
||||
this.volumeService.deleteMultipleVolumes(volumes.map(v => v.id)).subscribe((success) => {
|
||||
if (callback) {
|
||||
|
|
@ -536,7 +535,7 @@ export class ActionService {
|
|||
|
||||
addMultipleSeriesToWantToReadList(seriesIds: Array<number>, callback?: VoidActionCallback) {
|
||||
this.memberService.addSeriesToWantToRead(seriesIds).subscribe(() => {
|
||||
this.toastr.success('Series added to Want to Read list');
|
||||
this.toastr.success(translate('toasts.series-added-want-to-read'));
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,7 +109,11 @@ export enum EVENTS {
|
|||
/**
|
||||
* A Progress event when a smart collection is synchronizing
|
||||
*/
|
||||
SmartCollectionSync = 'SmartCollectionSync'
|
||||
SmartCollectionSync = 'SmartCollectionSync',
|
||||
/**
|
||||
* A Person merged has been merged into another
|
||||
*/
|
||||
PersonMerged = 'PersonMerged',
|
||||
}
|
||||
|
||||
export interface Message<T> {
|
||||
|
|
@ -336,6 +340,13 @@ export class MessageHubService {
|
|||
payload: resp.body
|
||||
});
|
||||
});
|
||||
|
||||
this.hubConnection.on(EVENTS.PersonMerged, resp => {
|
||||
this.messagesSource.next({
|
||||
event: EVENTS.PersonMerged,
|
||||
payload: resp.body
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
stopHubConnection() {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from "@angular/common/http";
|
||||
import {Injectable} from '@angular/core';
|
||||
import {HttpClient, HttpParams} from "@angular/common/http";
|
||||
import {environment} from "../../environments/environment";
|
||||
import {Person, PersonRole} from "../_models/metadata/person";
|
||||
import {SeriesFilterV2} from "../_models/metadata/v2/series-filter-v2";
|
||||
import {PaginatedResult} from "../_models/pagination";
|
||||
import {Series} from "../_models/series";
|
||||
import {map} from "rxjs/operators";
|
||||
import {UtilityService} from "../shared/_services/utility.service";
|
||||
import {BrowsePerson} from "../_models/person/browse-person";
|
||||
import {Chapter} from "../_models/chapter";
|
||||
import {StandaloneChapter} from "../_models/standalone-chapter";
|
||||
import {TextResonse} from "../_types/text-response";
|
||||
|
||||
|
|
@ -29,6 +27,10 @@ export class PersonService {
|
|||
return this.httpClient.get<Person | null>(this.baseUrl + `person?name=${name}`);
|
||||
}
|
||||
|
||||
searchPerson(name: string) {
|
||||
return this.httpClient.get<Array<Person>>(this.baseUrl + `person/search?queryString=${encodeURIComponent(name)}`);
|
||||
}
|
||||
|
||||
getRolesForPerson(personId: number) {
|
||||
return this.httpClient.get<Array<PersonRole>>(this.baseUrl + `person/roles?personId=${personId}`);
|
||||
}
|
||||
|
|
@ -55,4 +57,15 @@ export class PersonService {
|
|||
downloadCover(personId: number) {
|
||||
return this.httpClient.post<string>(this.baseUrl + 'person/fetch-cover?personId=' + personId, {}, TextResonse);
|
||||
}
|
||||
|
||||
isValidAlias(personId: number, alias: string) {
|
||||
return this.httpClient.get<boolean>(this.baseUrl + `person/valid-alias?personId=${personId}&alias=${alias}`, TextResonse).pipe(
|
||||
map(valid => valid + '' === 'true')
|
||||
);
|
||||
}
|
||||
|
||||
mergePerson(destId: number, srcId: number) {
|
||||
return this.httpClient.post<Person>(this.baseUrl + 'person/merge', {destId, srcId});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,19 @@
|
|||
import { HttpClient } from '@angular/common/http';
|
||||
import {HttpClient, HttpParams} from '@angular/common/http';
|
||||
import {Inject, inject, Injectable} from '@angular/core';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { UserReadStatistics } from '../statistics/_models/user-read-statistics';
|
||||
import { PublicationStatusPipe } from '../_pipes/publication-status.pipe';
|
||||
import {asyncScheduler, finalize, map, tap} from 'rxjs';
|
||||
import { MangaFormatPipe } from '../_pipes/manga-format.pipe';
|
||||
import { FileExtensionBreakdown } from '../statistics/_models/file-breakdown';
|
||||
import { TopUserRead } from '../statistics/_models/top-reads';
|
||||
import { ReadHistoryEvent } from '../statistics/_models/read-history-event';
|
||||
import { ServerStatistics } from '../statistics/_models/server-statistics';
|
||||
import { StatCount } from '../statistics/_models/stat-count';
|
||||
import { PublicationStatus } from '../_models/metadata/publication-status';
|
||||
import { MangaFormat } from '../_models/manga-format';
|
||||
import { TextResonse } from '../_types/text-response';
|
||||
import {environment} from 'src/environments/environment';
|
||||
import {UserReadStatistics} from '../statistics/_models/user-read-statistics';
|
||||
import {PublicationStatusPipe} from '../_pipes/publication-status.pipe';
|
||||
import {asyncScheduler, map} from 'rxjs';
|
||||
import {MangaFormatPipe} from '../_pipes/manga-format.pipe';
|
||||
import {FileExtensionBreakdown} from '../statistics/_models/file-breakdown';
|
||||
import {TopUserRead} from '../statistics/_models/top-reads';
|
||||
import {ReadHistoryEvent} from '../statistics/_models/read-history-event';
|
||||
import {ServerStatistics} from '../statistics/_models/server-statistics';
|
||||
import {StatCount} from '../statistics/_models/stat-count';
|
||||
import {PublicationStatus} from '../_models/metadata/publication-status';
|
||||
import {MangaFormat} from '../_models/manga-format';
|
||||
import {TextResonse} from '../_types/text-response';
|
||||
import {TranslocoService} from "@jsverse/transloco";
|
||||
import {KavitaPlusMetadataBreakdown} from "../statistics/_models/kavitaplus-metadata-breakdown";
|
||||
import {throttleTime} from "rxjs/operators";
|
||||
import {DEBOUNCE_TIME} from "../shared/_services/download.service";
|
||||
import {download} from "../shared/_models/download";
|
||||
|
|
@ -44,11 +43,14 @@ export class StatisticsService {
|
|||
constructor(private httpClient: HttpClient, @Inject(SAVER) private save: Saver) { }
|
||||
|
||||
getUserStatistics(userId: number, libraryIds: Array<number> = []) {
|
||||
// TODO: Convert to httpParams object
|
||||
let url = 'stats/user/' + userId + '/read';
|
||||
if (libraryIds.length > 0) url += '?libraryIds=' + libraryIds.join(',');
|
||||
const url = `${this.baseUrl}stats/user/${userId}/read`;
|
||||
|
||||
return this.httpClient.get<UserReadStatistics>(this.baseUrl + url);
|
||||
let params = new HttpParams();
|
||||
if (libraryIds.length > 0) {
|
||||
params = params.set('libraryIds', libraryIds.join(','));
|
||||
}
|
||||
|
||||
return this.httpClient.get<UserReadStatistics>(url, { params });
|
||||
}
|
||||
|
||||
getServerStatistics() {
|
||||
|
|
@ -59,7 +61,7 @@ export class StatisticsService {
|
|||
return this.httpClient.get<StatCount<number>[]>(this.baseUrl + 'stats/server/count/year').pipe(
|
||||
map(spreads => spreads.map(spread => {
|
||||
return {name: spread.value + '', value: spread.count};
|
||||
})));
|
||||
})));
|
||||
}
|
||||
|
||||
getTopYears() {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
<ng-container *transloco="let t; read: 'actionable'">
|
||||
<div class="modal-container">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{t('title')}}</h4>
|
||||
<h4 class="modal-title">
|
||||
{{t('title')}}
|
||||
</h4>
|
||||
<button type="button" class="btn-close" aria-label="close" (click)="modal.close()"></button>
|
||||
</div>
|
||||
<div class="modal-body scrollable-modal">
|
||||
|
|
@ -12,8 +14,6 @@
|
|||
}
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
|
||||
|
||||
@for (action of currentItems; track action.title) {
|
||||
@if (willRenderAction(action)) {
|
||||
<button class="btn btn-outline-primary text-start d-flex justify-content-between align-items-center w-100"
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component, DestroyRef,
|
||||
Component,
|
||||
DestroyRef,
|
||||
EventEmitter,
|
||||
inject,
|
||||
Input,
|
||||
OnInit,
|
||||
Output
|
||||
} from '@angular/core';
|
||||
import {NgClass} from "@angular/common";
|
||||
import {translate, TranslocoDirective} from "@jsverse/transloco";
|
||||
import {Breakpoint, UtilityService} from "../../shared/_services/utility.service";
|
||||
import {NgbActiveModal} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {Action, ActionItem} from "../../_services/action-factory.service";
|
||||
import {ActionableEntity, ActionItem} from "../../_services/action-factory.service";
|
||||
import {AccountService} from "../../_services/account.service";
|
||||
import {tap} from "rxjs";
|
||||
import {User} from "../../_models/user";
|
||||
|
|
@ -36,6 +36,7 @@ export class ActionableModalComponent implements OnInit {
|
|||
protected readonly destroyRef = inject(DestroyRef);
|
||||
protected readonly Breakpoint = Breakpoint;
|
||||
|
||||
@Input() entity: ActionableEntity = null;
|
||||
@Input() actions: ActionItem<any>[] = [];
|
||||
@Input() willRenderAction!: (action: ActionItem<any>) => boolean;
|
||||
@Input() shouldRenderSubMenu!: (action: ActionItem<any>, dynamicList: null | Array<any>) => boolean;
|
||||
|
|
|
|||
|
|
@ -1,51 +1,57 @@
|
|||
<ng-container *transloco="let t; read: 'actionable'">
|
||||
@if (actions.length > 0) {
|
||||
@if ((utilityService.activeBreakpoint$ | async)! <= Breakpoint.Tablet) {
|
||||
<button [disabled]="disabled" class="btn {{btnClass}} px-3" id="actions-{{labelBy}}"
|
||||
(click)="openMobileActionableMenu($event)">
|
||||
{{label}}
|
||||
<i class="fa {{iconClass}}" aria-hidden="true"></i>
|
||||
</button>
|
||||
} @else {
|
||||
<div ngbDropdown container="body" class="d-inline-block">
|
||||
<button [disabled]="disabled" class="btn {{btnClass}} px-3" id="actions-{{labelBy}}" ngbDropdownToggle
|
||||
(click)="preventEvent($event)">
|
||||
<ng-container *transloco="let t; read: 'actionable'">
|
||||
@if (actions.length > 0) {
|
||||
@if ((utilityService.activeBreakpoint$ | async)! <= Breakpoint.Tablet) {
|
||||
<button [disabled]="disabled" class="btn {{btnClass}} px-3" id="actions-{{labelBy}}"
|
||||
(click)="openMobileActionableMenu($event)">
|
||||
{{label}}
|
||||
<i class="fa {{iconClass}}" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div ngbDropdownMenu attr.aria-labelledby="actions-{{labelBy}}">
|
||||
<ng-container *ngTemplateOutlet="submenu; context: { list: actions }"></ng-container>
|
||||
} @else {
|
||||
<div ngbDropdown container="body" class="d-inline-block">
|
||||
<button [disabled]="disabled" class="btn {{btnClass}} px-3" id="actions-{{labelBy}}" ngbDropdownToggle
|
||||
(click)="preventEvent($event)">
|
||||
{{label}}
|
||||
<i class="fa {{iconClass}}" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div ngbDropdownMenu attr.aria-labelledby="actions-{{labelBy}}">
|
||||
<ng-container *ngTemplateOutlet="submenu; context: { list: actions }"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #submenu let-list="list">
|
||||
@for(action of list; track action.title) {
|
||||
<!-- Non Submenu items -->
|
||||
@if (action.children === undefined || action?.children?.length === 0 || action.dynamicList !== undefined) {
|
||||
@if (action.dynamicList !== undefined && (action.dynamicList | async | dynamicList); as dList) {
|
||||
@for(dynamicItem of dList; track dynamicItem.title) {
|
||||
<button ngbDropdownItem (click)="performDynamicClick($event, action, dynamicItem)">{{dynamicItem.title}}</button>
|
||||
}
|
||||
} @else if (willRenderAction(action)) {
|
||||
<button ngbDropdownItem (click)="performAction($event, action)" (mouseover)="closeAllSubmenus()">{{t(action.title)}}</button>
|
||||
}
|
||||
} @else {
|
||||
@if (shouldRenderSubMenu(action, action.children?.[0].dynamicList | async)) {
|
||||
<!-- Submenu items -->
|
||||
<div ngbDropdown #subMenuHover="ngbDropdown" placement="right left"
|
||||
(click)="preventEvent($event); openSubmenu(action.title, subMenuHover)"
|
||||
(mouseover)="preventEvent($event); openSubmenu(action.title, subMenuHover)"
|
||||
(mouseleave)="preventEvent($event)">
|
||||
@if (willRenderAction(action)) {
|
||||
<button id="actions-{{action.title}}" class="submenu-toggle" ngbDropdownToggle>{{t(action.title)}} <i class="fa-solid fa-angle-right submenu-icon"></i></button>
|
||||
<ng-template #submenu let-list="list">
|
||||
@for(action of list; track action.title) {
|
||||
<!-- Non Submenu items -->
|
||||
@if (action.children === undefined || action?.children?.length === 0 || action.dynamicList !== undefined) {
|
||||
@if (action.dynamicList !== undefined && (action.dynamicList | async | dynamicList); as dList) {
|
||||
@for(dynamicItem of dList; track dynamicItem.title) {
|
||||
<button ngbDropdownItem (click)="performDynamicClick($event, action, dynamicItem)">{{dynamicItem.title}}</button>
|
||||
}
|
||||
<div ngbDropdownMenu attr.aria-labelledby="actions-{{action.title}}">
|
||||
<ng-container *ngTemplateOutlet="submenu; context: { list: action.children }"></ng-container>
|
||||
} @else if (willRenderAction(action, this.currentUser!)) {
|
||||
<button ngbDropdownItem (click)="performAction($event, action)">{{t(action.title)}}</button>
|
||||
}
|
||||
} @else {
|
||||
@if (shouldRenderSubMenu(action, action.children?.[0].dynamicList | async) && hasRenderableChildren(action, this.currentUser!)) {
|
||||
<!-- Submenu items -->
|
||||
<div ngbDropdown #subMenuHover="ngbDropdown" placement="right left"
|
||||
(click)="openSubmenu(action.title, subMenuHover)"
|
||||
(mouseenter)="openSubmenu(action.title, subMenuHover)"
|
||||
(mouseover)="preventEvent($event)"
|
||||
class="submenu-wrapper">
|
||||
|
||||
<!-- Check to ensure the submenu has items -->
|
||||
@if (willRenderAction(action, this.currentUser!)) {
|
||||
<button id="actions-{{action.title}}" class="submenu-toggle" ngbDropdownToggle>
|
||||
{{t(action.title)}} <i class="fa-solid fa-angle-right submenu-icon"></i>
|
||||
</button>
|
||||
}
|
||||
|
||||
<div ngbDropdownMenu attr.aria-labelledby="actions-{{action.title}}">
|
||||
<ng-container *ngTemplateOutlet="submenu; context: { list: action.children }"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
}
|
||||
}
|
||||
}
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,22 @@
|
|||
content: none !important;
|
||||
}
|
||||
|
||||
.submenu-wrapper {
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -10px;
|
||||
width: 10px;
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.submenu-toggle {
|
||||
display: block;
|
||||
width: 100%;
|
||||
|
|
@ -30,9 +46,3 @@
|
|||
.btn {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
// Robbie added this but it broke most of the uses
|
||||
//.dropdown-toggle {
|
||||
// padding-top: 0;
|
||||
// padding-bottom: 0;
|
||||
//}
|
||||
|
|
|
|||
|
|
@ -1,31 +1,39 @@
|
|||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component, DestroyRef,
|
||||
Component,
|
||||
DestroyRef,
|
||||
EventEmitter,
|
||||
inject,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output
|
||||
} from '@angular/core';
|
||||
import {NgbDropdown, NgbDropdownItem, NgbDropdownMenu, NgbDropdownToggle, NgbModal} from '@ng-bootstrap/ng-bootstrap';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
import { Action, ActionItem } from 'src/app/_services/action-factory.service';
|
||||
import {AccountService} from 'src/app/_services/account.service';
|
||||
import {ActionableEntity, ActionItem} from 'src/app/_services/action-factory.service';
|
||||
import {AsyncPipe, NgTemplateOutlet} from "@angular/common";
|
||||
import {TranslocoDirective} from "@jsverse/transloco";
|
||||
import {DynamicListPipe} from "./_pipes/dynamic-list.pipe";
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import {Breakpoint, UtilityService} from "../../shared/_services/utility.service";
|
||||
import {ActionableModalComponent} from "../actionable-modal/actionable-modal.component";
|
||||
import {User} from "../../_models/user";
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-card-actionables',
|
||||
imports: [NgbDropdown, NgbDropdownToggle, NgbDropdownMenu, NgbDropdownItem, DynamicListPipe, TranslocoDirective, AsyncPipe, NgTemplateOutlet],
|
||||
templateUrl: './card-actionables.component.html',
|
||||
styleUrls: ['./card-actionables.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
selector: 'app-card-actionables',
|
||||
imports: [
|
||||
NgbDropdown, NgbDropdownToggle, NgbDropdownMenu, NgbDropdownItem,
|
||||
DynamicListPipe, TranslocoDirective, AsyncPipe, NgTemplateOutlet
|
||||
],
|
||||
templateUrl: './card-actionables.component.html',
|
||||
styleUrls: ['./card-actionables.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class CardActionablesComponent implements OnInit {
|
||||
export class CardActionablesComponent implements OnInit, OnChanges, OnDestroy {
|
||||
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly accountService = inject(AccountService);
|
||||
|
|
@ -37,58 +45,69 @@ export class CardActionablesComponent implements OnInit {
|
|||
|
||||
@Input() iconClass = 'fa-ellipsis-v';
|
||||
@Input() btnClass = '';
|
||||
@Input() actions: ActionItem<any>[] = [];
|
||||
@Input() inputActions: ActionItem<any>[] = [];
|
||||
@Input() labelBy = 'card';
|
||||
/**
|
||||
* Text to display as if actionable was a button
|
||||
*/
|
||||
@Input() label = '';
|
||||
@Input() disabled: boolean = false;
|
||||
|
||||
@Input() entity: ActionableEntity = null;
|
||||
/**
|
||||
* This will only emit when the action is clicked and the entity is null. Otherwise, the entity callback handler will be invoked.
|
||||
*/
|
||||
@Output() actionHandler = new EventEmitter<ActionItem<any>>();
|
||||
|
||||
|
||||
isAdmin: boolean = false;
|
||||
canDownload: boolean = false;
|
||||
canPromote: boolean = false;
|
||||
actions: ActionItem<ActionableEntity>[] = [];
|
||||
currentUser: User | undefined = undefined;
|
||||
submenu: {[key: string]: NgbDropdown} = {};
|
||||
private closeTimeout: any = null;
|
||||
|
||||
|
||||
ngOnInit(): void {
|
||||
this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((user) => {
|
||||
if (!user) return;
|
||||
this.isAdmin = this.accountService.hasAdminRole(user);
|
||||
this.canDownload = this.accountService.hasDownloadRole(user);
|
||||
this.canPromote = this.accountService.hasPromoteRole(user);
|
||||
|
||||
// We want to avoid an empty menu when user doesn't have access to anything
|
||||
if (!this.isAdmin && this.actions.filter(a => !a.requiresAdmin).length === 0) {
|
||||
this.actions = [];
|
||||
}
|
||||
|
||||
this.currentUser = user;
|
||||
this.actions = this.inputActions.filter(a => this.willRenderAction(a, user!));
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
this.actions = this.inputActions.filter(a => this.willRenderAction(a, this.currentUser!));
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.cancelCloseSubmenus();
|
||||
}
|
||||
|
||||
preventEvent(event: any) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
performAction(event: any, action: ActionItem<any>) {
|
||||
performAction(event: any, action: ActionItem<ActionableEntity>) {
|
||||
this.preventEvent(event);
|
||||
|
||||
if (typeof action.callback === 'function') {
|
||||
this.actionHandler.emit(action);
|
||||
if (this.entity === null) {
|
||||
this.actionHandler.emit(action);
|
||||
} else {
|
||||
action.callback(action, this.entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
willRenderAction(action: ActionItem<any>) {
|
||||
return (action.requiresAdmin && this.isAdmin)
|
||||
|| (action.action === Action.Download && (this.canDownload || this.isAdmin))
|
||||
|| (!action.requiresAdmin && action.action !== Action.Download)
|
||||
|| (action.action === Action.Promote && (this.canPromote || this.isAdmin))
|
||||
|| (action.action === Action.UnPromote && (this.canPromote || this.isAdmin))
|
||||
;
|
||||
/**
|
||||
* The user has required roles (or no roles defined) and action shouldRender returns true
|
||||
* @param action
|
||||
* @param user
|
||||
*/
|
||||
willRenderAction(action: ActionItem<ActionableEntity>, user: User) {
|
||||
return (!action.requiredRoles?.length || this.accountService.hasAnyRole(user, action.requiredRoles)) && action.shouldRender(action, this.entity, user);
|
||||
}
|
||||
|
||||
shouldRenderSubMenu(action: ActionItem<any>, dynamicList: null | Array<any>) {
|
||||
|
|
@ -109,13 +128,41 @@ export class CardActionablesComponent implements OnInit {
|
|||
}
|
||||
|
||||
closeAllSubmenus() {
|
||||
Object.keys(this.submenu).forEach(key => {
|
||||
this.submenu[key].close();
|
||||
// Clear any existing timeout to avoid race conditions
|
||||
if (this.closeTimeout) {
|
||||
clearTimeout(this.closeTimeout);
|
||||
}
|
||||
|
||||
// Set a new timeout to close submenus after a short delay
|
||||
this.closeTimeout = setTimeout(() => {
|
||||
Object.keys(this.submenu).forEach(key => {
|
||||
this.submenu[key].close();
|
||||
delete this.submenu[key];
|
||||
});
|
||||
});
|
||||
}, 100); // Small delay to prevent premature closing (dropdown tunneling)
|
||||
}
|
||||
|
||||
performDynamicClick(event: any, action: ActionItem<any>, dynamicItem: any) {
|
||||
cancelCloseSubmenus() {
|
||||
if (this.closeTimeout) {
|
||||
clearTimeout(this.closeTimeout);
|
||||
this.closeTimeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
hasRenderableChildren(action: ActionItem<ActionableEntity>, user: User): boolean {
|
||||
if (!action.children || action.children.length === 0) return false;
|
||||
|
||||
for (const child of action.children) {
|
||||
const dynamicList = child.dynamicList;
|
||||
if (dynamicList !== undefined) return true; // Dynamic list gets rendered if loaded
|
||||
|
||||
if (this.willRenderAction(child, user)) return true;
|
||||
if (child.children?.length && this.hasRenderableChildren(child, user)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
performDynamicClick(event: any, action: ActionItem<ActionableEntity>, dynamicItem: any) {
|
||||
action._extra = dynamicItem;
|
||||
this.performAction(event, action);
|
||||
}
|
||||
|
|
@ -124,6 +171,7 @@ export class CardActionablesComponent implements OnInit {
|
|||
this.preventEvent(event);
|
||||
|
||||
const ref = this.modalService.open(ActionableModalComponent, {fullscreen: true, centered: true});
|
||||
ref.componentInstance.entity = this.entity;
|
||||
ref.componentInstance.actions = this.actions;
|
||||
ref.componentInstance.willRenderAction = this.willRenderAction.bind(this);
|
||||
ref.componentInstance.shouldRenderSubMenu = this.shouldRenderSubMenu.bind(this);
|
||||
|
|
|
|||
|
|
@ -483,7 +483,7 @@ export class EditChapterModalComponent implements OnInit {
|
|||
};
|
||||
|
||||
personSettings.addTransformFn = ((title: string) => {
|
||||
return {id: 0, name: title, role: role, description: '', coverImage: '', coverImageLocked: false, primaryColor: '', secondaryColor: '' };
|
||||
return {id: 0, name: title, aliases: [], role: role, description: '', coverImage: '', coverImageLocked: false, primaryColor: '', secondaryColor: '' };
|
||||
});
|
||||
|
||||
personSettings.trackByIdentityFn = (index, value) => value.name + (value.id + '');
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@
|
|||
<app-match-series-result-item [item]="item" [isDarkMode]="(themeService.isDarkMode$ | async)!" (selected)="selectMatch($event)"></app-match-series-result-item>
|
||||
} @empty {
|
||||
@if (!isLoading) {
|
||||
{{t('no-results')}}
|
||||
<p>{{t('no-results')}}</p>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
<ng-container *transloco="let t; read: 'manage-library'">
|
||||
<div class="position-relative">
|
||||
<div class="position-absolute custom-position-2">
|
||||
<app-card-actionables [actions]="bulkActions" btnClass="btn-outline-primary ms-1" [label]="t('bulk-action-label')" [disabled]="bulkMode" (actionHandler)="handleBulkAction($event, null)">
|
||||
<app-card-actionables [inputActions]="bulkActions" btnClass="btn-outline-primary ms-1" [label]="t('bulk-action-label')"
|
||||
[disabled]="bulkMode">
|
||||
</app-card-actionables>
|
||||
</div>
|
||||
|
||||
|
|
@ -72,11 +73,22 @@
|
|||
<td>
|
||||
<!-- On Mobile we want to use ... for each row -->
|
||||
@if (useActionables$ | async) {
|
||||
<app-card-actionables [actions]="actions" (actionHandler)="performAction($event, library)"></app-card-actionables>
|
||||
<app-card-actionables [entity]="library" [inputActions]="actions"></app-card-actionables>
|
||||
} @else {
|
||||
<button class="btn btn-secondary me-2 btn-sm" (click)="scanLibrary(library)" placement="top" [ngbTooltip]="t('scan-library')" [attr.aria-label]="t('scan-library')"><i class="fa fa-sync-alt" aria-hidden="true"></i></button>
|
||||
<button class="btn btn-danger me-2 btn-sm" [disabled]="deletionInProgress" (click)="deleteLibrary(library)"><i class="fa fa-trash" placement="top" [ngbTooltip]="t('delete-library')" [attr.aria-label]="t('delete-library-by-name', {name: library.name | sentenceCase})"></i></button>
|
||||
<button class="btn btn-primary btn-sm" (click)="editLibrary(library)"><i class="fa fa-pen" placement="top" [ngbTooltip]="t('edit-library')" [attr.aria-label]="t('edit-library-by-name', {name: library.name | sentenceCase})"></i></button>
|
||||
<button class="btn btn-secondary me-2 btn-sm" (click)="scanLibrary(library)" placement="top" [ngbTooltip]="t('scan-library')"
|
||||
[attr.aria-label]="t('scan-library')">
|
||||
<i class="fa fa-sync-alt" aria-hidden="true"></i>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-danger me-2 btn-sm" [disabled]="deletionInProgress" (click)="deleteLibrary(library)">
|
||||
<i class="fa fa-trash" placement="top" [ngbTooltip]="t('delete-library')"
|
||||
[attr.aria-label]="t('delete-library-by-name', {name: library.name | sentenceCase})"></i>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-primary btn-sm" (click)="editLibrary(library)">
|
||||
<i class="fa fa-pen" placement="top" [ngbTooltip]="t('edit-library')"
|
||||
[attr.aria-label]="t('edit-library-by-name', {name: library.name | sentenceCase})"></i>
|
||||
</button>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
|||
|
|
@ -83,12 +83,12 @@ export class ManageLibraryComponent implements OnInit {
|
|||
lastSelectedIndex: number | null = null;
|
||||
|
||||
@HostListener('document:keydown.shift', ['$event'])
|
||||
handleKeypress(event: KeyboardEvent) {
|
||||
handleKeypress(_: KeyboardEvent) {
|
||||
this.isShiftDown = true;
|
||||
}
|
||||
|
||||
@HostListener('document:keyup.shift', ['$event'])
|
||||
handleKeyUp(event: KeyboardEvent) {
|
||||
handleKeyUp(_: KeyboardEvent) {
|
||||
this.isShiftDown = false;
|
||||
}
|
||||
|
||||
|
|
@ -106,7 +106,7 @@ export class ManageLibraryComponent implements OnInit {
|
|||
ngOnInit(): void {
|
||||
this.getLibraries();
|
||||
|
||||
// when a progress event comes in, show it on the UI next to library
|
||||
// when a progress event comes in, show it on the UI next to the library
|
||||
this.hubService.messages$.pipe(takeUntilDestroyed(this.destroyRef),
|
||||
filter(event => event.event === EVENTS.ScanSeries || event.event === EVENTS.NotificationProgress),
|
||||
distinctUntilChanged((prev: Message<ScanSeriesEvent | NotificationProgressEvent>, curr: Message<ScanSeriesEvent | NotificationProgressEvent>) =>
|
||||
|
|
@ -270,7 +270,8 @@ export class ManageLibraryComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
async handleBulkAction(action: ActionItem<Library>, library : Library | null) {
|
||||
async handleBulkAction(action: ActionItem<Library>, _: Library) {
|
||||
//Library is null for bulk actions
|
||||
this.bulkAction = action.action;
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
|
|
@ -284,7 +285,7 @@ export class ManageLibraryComponent implements OnInit {
|
|||
break;
|
||||
case (Action.CopySettings):
|
||||
|
||||
// Prompt the user for the library then wait for them to manually trigger applyBulkAction
|
||||
// Prompt the user for the library, then wait for them to manually trigger applyBulkAction
|
||||
const ref = this.modalService.open(CopySettingsFromLibraryModalComponent, {size: 'lg', fullscreen: 'md'});
|
||||
ref.componentInstance.libraries = this.libraries;
|
||||
ref.closed.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((res: number | null) => {
|
||||
|
|
@ -298,7 +299,6 @@ export class ManageLibraryComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
async handleAction(action: ActionItem<Library>, library: Library) {
|
||||
switch (action.action) {
|
||||
case(Action.Scan):
|
||||
|
|
@ -321,13 +321,6 @@ export class ManageLibraryComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
performAction(action: ActionItem<Library>, library: Library) {
|
||||
if (typeof action.callback === 'function') {
|
||||
action.callback(action, library);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
setupSelections() {
|
||||
this.selections = new SelectionModel<Library>(false, this.libraries);
|
||||
this.cdRef.markForCheck();
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
@if (logs$ | async; as items) {
|
||||
<virtual-scroller #scroll [items]="items" [bufferAmount]="1">
|
||||
<div class="grid row g-0" #container>
|
||||
@for (item of scroll.viewPortItems; track item.timestamp) {
|
||||
<div class="card col-auto mt-2 mb-2">
|
||||
{{item.timestamp | date}} [{{item.level}}] {{item.message}}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</virtual-scroller>
|
||||
}
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { HubConnection, HubConnectionBuilder } from '@microsoft/signalr';
|
||||
import { BehaviorSubject, take } from 'rxjs';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { VirtualScrollerModule } from '@iharbeck/ngx-virtual-scroller';
|
||||
import { AsyncPipe, DatePipe } from '@angular/common';
|
||||
|
||||
interface LogMessage {
|
||||
timestamp: string;
|
||||
level: 'Information' | 'Debug' | 'Warning' | 'Error';
|
||||
message: string;
|
||||
exception: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-manage-logs',
|
||||
templateUrl: './manage-logs.component.html',
|
||||
styleUrls: ['./manage-logs.component.scss'],
|
||||
standalone: true,
|
||||
imports: [VirtualScrollerModule, AsyncPipe, DatePipe]
|
||||
})
|
||||
export class ManageLogsComponent implements OnInit, OnDestroy {
|
||||
|
||||
hubUrl = environment.hubUrl;
|
||||
private hubConnection!: HubConnection;
|
||||
|
||||
logsSource = new BehaviorSubject<LogMessage[]>([]);
|
||||
public logs$ = this.logsSource.asObservable();
|
||||
|
||||
constructor(private accountService: AccountService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// TODO: Come back and implement this one day
|
||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||
if (user) {
|
||||
this.hubConnection = new HubConnectionBuilder()
|
||||
.withUrl(this.hubUrl + 'logs', {
|
||||
accessTokenFactory: () => user.token
|
||||
})
|
||||
.withAutomaticReconnect()
|
||||
.build();
|
||||
|
||||
console.log('Starting log connection');
|
||||
|
||||
this.hubConnection
|
||||
.start()
|
||||
.catch(err => console.error(err));
|
||||
|
||||
this.hubConnection.on('SendLogAsObject', resp => {
|
||||
const payload = resp.arguments[0] as LogMessage;
|
||||
const logMessage = {timestamp: payload.timestamp, level: payload.level, message: payload.message, exception: payload.exception};
|
||||
// NOTE: It might be better to just have a queue to show this
|
||||
const values = this.logsSource.getValue();
|
||||
values.push(logMessage);
|
||||
this.logsSource.next(values);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// unsubscribe from signalr connection
|
||||
if (this.hubConnection) {
|
||||
this.hubConnection.stop().catch(err => console.error(err));
|
||||
console.log('Stopping log connection');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -10,6 +10,4 @@ import {ChangeDetectionStrategy, Component, Input} from '@angular/core';
|
|||
export class UpdateSectionComponent {
|
||||
@Input({required: true}) items: Array<string> = [];
|
||||
@Input({required: true}) title: string = '';
|
||||
|
||||
// TODO: Implement a read-more-list so that we by default show a configurable number
|
||||
}
|
||||
|
|
|
|||
|
|
@ -521,7 +521,7 @@ export class EditSeriesModalComponent implements OnInit {
|
|||
};
|
||||
|
||||
personSettings.addTransformFn = ((title: string) => {
|
||||
return {id: 0, name: title, description: '', coverImageLocked: false, primaryColor: '', secondaryColor: '' };
|
||||
return {id: 0, name: title, aliases: [], description: '', coverImageLocked: false, primaryColor: '', secondaryColor: '' };
|
||||
});
|
||||
personSettings.trackByIdentityFn = (index, value) => value.name + (value.id + '');
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
<span class="visually-hidden">{{t('mark-as-read')}}</span>
|
||||
</button>
|
||||
}
|
||||
<app-card-actionables [actions]="actions" labelBy="bulk-actions-header" class="actionables" iconClass="fa-ellipsis-h" (actionHandler)="performAction($event)"></app-card-actionables>
|
||||
<app-card-actionables (actionHandler)="performAction($event)" [inputActions]="actions" labelBy="bulk-actions-header" iconClass="fa-ellipsis-h" class="actionables"/>
|
||||
</span>
|
||||
|
||||
<span id="bulk-actions-header" class="visually-hidden">Bulk Actions</span>
|
||||
|
|
|
|||
|
|
@ -2,13 +2,14 @@ import {
|
|||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
DestroyRef, HostListener,
|
||||
DestroyRef,
|
||||
HostListener,
|
||||
inject,
|
||||
Input,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { Action, ActionFactoryService, ActionItem } from 'src/app/_services/action-factory.service';
|
||||
import { BulkSelectionService } from '../bulk-selection.service';
|
||||
import {Action, ActionFactoryService, ActionItem} from 'src/app/_services/action-factory.service';
|
||||
import {BulkSelectionService} from '../bulk-selection.service';
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import {AsyncPipe, DecimalPipe, NgStyle} from "@angular/common";
|
||||
import {TranslocoModule} from "@jsverse/transloco";
|
||||
|
|
@ -17,18 +18,18 @@ import {CardActionablesComponent} from "../../_single-module/card-actionables/ca
|
|||
import {KEY_CODES} from "../../shared/_services/utility.service";
|
||||
|
||||
@Component({
|
||||
selector: 'app-bulk-operations',
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
CardActionablesComponent,
|
||||
TranslocoModule,
|
||||
NgbTooltip,
|
||||
NgStyle,
|
||||
DecimalPipe
|
||||
],
|
||||
templateUrl: './bulk-operations.component.html',
|
||||
styleUrls: ['./bulk-operations.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
selector: 'app-bulk-operations',
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
CardActionablesComponent,
|
||||
TranslocoModule,
|
||||
NgbTooltip,
|
||||
NgStyle,
|
||||
DecimalPipe
|
||||
],
|
||||
templateUrl: './bulk-operations.component.html',
|
||||
styleUrls: ['./bulk-operations.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class BulkOperationsComponent implements OnInit {
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
<h4>
|
||||
@if (actions.length > 0) {
|
||||
<span>
|
||||
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="header"></app-card-actionables>
|
||||
<app-card-actionables (actionHandler)="performAction($event)" [inputActions]="actions" [labelBy]="header"></app-card-actionables>
|
||||
</span>
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@
|
|||
|
||||
<span class="card-actions">
|
||||
@if (actions && actions.length > 0) {
|
||||
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="title"></app-card-actionables>
|
||||
<app-card-actionables (actionHandler)="performAction($event)" [inputActions]="actions" [labelBy]="title"></app-card-actionables>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -344,10 +344,6 @@ export class CardItemComponent implements OnInit {
|
|||
this.clicked.emit(this.title);
|
||||
}
|
||||
|
||||
preventClick(event: any) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
performAction(action: ActionItem<any>) {
|
||||
if (action.action == Action.Download) {
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@
|
|||
</span>
|
||||
<span class="card-actions">
|
||||
@if (actions && actions.length > 0) {
|
||||
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="chapter.titleName"></app-card-actionables>
|
||||
<app-card-actionables [entity]="chapter" [inputActions]="actions" [labelBy]="chapter.titleName"></app-card-actionables>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@ import {
|
|||
ChangeDetectorRef,
|
||||
Component,
|
||||
DestroyRef,
|
||||
EventEmitter, HostListener,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
inject,
|
||||
Input, OnInit,
|
||||
Input,
|
||||
OnInit,
|
||||
Output
|
||||
} from '@angular/core';
|
||||
import {ImageService} from "../../_services/image.service";
|
||||
|
|
@ -14,7 +16,7 @@ import {DownloadEvent, DownloadService} from "../../shared/_services/download.se
|
|||
import {EVENTS, MessageHubService} from "../../_services/message-hub.service";
|
||||
import {AccountService} from "../../_services/account.service";
|
||||
import {ScrollService} from "../../_services/scroll.service";
|
||||
import {Action, ActionFactoryService, ActionItem} from "../../_services/action-factory.service";
|
||||
import {ActionItem} from "../../_services/action-factory.service";
|
||||
import {Chapter} from "../../_models/chapter";
|
||||
import {Observable} from "rxjs";
|
||||
import {User} from "../../_models/user";
|
||||
|
|
@ -28,13 +30,10 @@ import {EntityTitleComponent} from "../entity-title/entity-title.component";
|
|||
import {CardActionablesComponent} from "../../_single-module/card-actionables/card-actionables.component";
|
||||
import {Router, RouterLink} from "@angular/router";
|
||||
import {TranslocoDirective} from "@jsverse/transloco";
|
||||
import {DefaultValuePipe} from "../../_pipes/default-value.pipe";
|
||||
import {filter, map} from "rxjs/operators";
|
||||
import {UserProgressUpdateEvent} from "../../_models/events/user-progress-update-event";
|
||||
import {ReaderService} from "../../_services/reader.service";
|
||||
import {LibraryType} from "../../_models/library/library";
|
||||
import {Device} from "../../_models/device/device";
|
||||
import {ActionService} from "../../_services/action.service";
|
||||
import {MangaFormat} from "../../_models/manga-format";
|
||||
|
||||
@Component({
|
||||
|
|
@ -60,15 +59,16 @@ export class ChapterCardComponent implements OnInit {
|
|||
public readonly imageService = inject(ImageService);
|
||||
public readonly bulkSelectionService = inject(BulkSelectionService);
|
||||
private readonly downloadService = inject(DownloadService);
|
||||
private readonly actionService = inject(ActionService);
|
||||
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);
|
||||
private readonly router = inject(Router);
|
||||
private readonly readerService = inject(ReaderService);
|
||||
|
||||
protected readonly LibraryType = LibraryType;
|
||||
protected readonly MangaFormat = MangaFormat;
|
||||
|
||||
@Input({required: true}) libraryId: number = 0;
|
||||
@Input({required: true}) seriesId: number = 0;
|
||||
@Input({required: true}) chapter!: Chapter;
|
||||
|
|
@ -143,8 +143,6 @@ export class ChapterCardComponent implements OnInit {
|
|||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.filterSendTo();
|
||||
|
||||
this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(user => {
|
||||
this.user = user;
|
||||
});
|
||||
|
|
@ -172,30 +170,6 @@ export class ChapterCardComponent implements OnInit {
|
|||
this.cdRef.detectChanges();
|
||||
}
|
||||
|
||||
|
||||
filterSendTo() {
|
||||
if (!this.actions || this.actions.length === 0) return;
|
||||
|
||||
this.actions = this.actionFactoryService.filterSendToAction(this.actions, this.chapter);
|
||||
}
|
||||
|
||||
performAction(action: ActionItem<any>) {
|
||||
if (action.action == Action.Download) {
|
||||
this.downloadService.download('chapter', this.chapter);
|
||||
return; // Don't propagate the download from a card
|
||||
}
|
||||
|
||||
if (action.action == Action.SendTo) {
|
||||
const device = (action._extra!.data as Device);
|
||||
this.actionService.sendToDevice([this.chapter.id], device);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof action.callback === 'function') {
|
||||
action.callback(action, this.chapter);
|
||||
}
|
||||
}
|
||||
|
||||
handleClick(event: any) {
|
||||
if (this.bulkSelectionService.hasSelections()) {
|
||||
this.handleSelection(event);
|
||||
|
|
@ -209,8 +183,4 @@ export class ChapterCardComponent implements OnInit {
|
|||
event.stopPropagation();
|
||||
this.readerService.readChapter(this.libraryId, this.seriesId, this.chapter, false);
|
||||
}
|
||||
|
||||
|
||||
protected readonly LibraryType = LibraryType;
|
||||
protected readonly MangaFormat = MangaFormat;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
</span>
|
||||
@if (actions && actions.length > 0) {
|
||||
<span class="card-actions float-end">
|
||||
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="title"></app-card-actionables>
|
||||
<app-card-actionables [entity]="entity" [inputActions]="actions" [labelBy]="title"></app-card-actionables>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component, ContentChild,
|
||||
DestroyRef, EventEmitter,
|
||||
Component,
|
||||
ContentChild,
|
||||
DestroyRef,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
inject,
|
||||
Input, Output, TemplateRef
|
||||
Input,
|
||||
Output,
|
||||
TemplateRef
|
||||
} from '@angular/core';
|
||||
import {Action, ActionFactoryService, ActionItem} from "../../_services/action-factory.service";
|
||||
import {ActionItem} from "../../_services/action-factory.service";
|
||||
import {ImageService} from "../../_services/image.service";
|
||||
import {BulkSelectionService} from "../bulk-selection.service";
|
||||
import {LibraryService} from "../../_services/library.service";
|
||||
import {DownloadService} from "../../shared/_services/download.service";
|
||||
import {UtilityService} from "../../shared/_services/utility.service";
|
||||
import {MessageHubService} from "../../_services/message-hub.service";
|
||||
import {AccountService} from "../../_services/account.service";
|
||||
import {ScrollService} from "../../_services/scroll.service";
|
||||
import {NgbTooltip} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {CardActionablesComponent} from "../../_single-module/card-actionables/card-actionables.component";
|
||||
|
|
@ -139,11 +139,6 @@ export class PersonCardComponent {
|
|||
this.clicked.emit(this.title);
|
||||
}
|
||||
|
||||
performAction(action: ActionItem<any>) {
|
||||
if (typeof action.callback === 'function') {
|
||||
action.callback(action, this.entity);
|
||||
}
|
||||
}
|
||||
|
||||
handleSelection(event?: any) {
|
||||
if (event) {
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@
|
|||
|
||||
@if (actions && actions.length > 0) {
|
||||
<span class="card-actions">
|
||||
<app-card-actionables (actionHandler)="handleSeriesActionCallback($event, series)" [actions]="actions" [labelBy]="series.name"></app-card-actionables>
|
||||
<app-card-actionables [entity]="series" [inputActions]="actions" [labelBy]="series.name"></app-card-actionables>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ import {ActionService} from 'src/app/_services/action.service';
|
|||
import {EditSeriesModalComponent} from '../_modals/edit-series-modal/edit-series-modal.component';
|
||||
import {RelationKind} from 'src/app/_models/series-detail/relation-kind';
|
||||
import {DecimalPipe} from "@angular/common";
|
||||
import {CardItemComponent} from "../card-item/card-item.component";
|
||||
import {RelationshipPipe} from "../../_pipes/relationship.pipe";
|
||||
import {Device} from "../../_models/device/device";
|
||||
import {translate, TranslocoDirective} from "@jsverse/transloco";
|
||||
|
|
@ -30,7 +29,6 @@ import {SeriesPreviewDrawerComponent} from "../../_single-module/series-preview-
|
|||
import {CardActionablesComponent} from "../../_single-module/card-actionables/card-actionables.component";
|
||||
import {DefaultValuePipe} from "../../_pipes/default-value.pipe";
|
||||
import {DownloadIndicatorComponent} from "../download-indicator/download-indicator.component";
|
||||
import {EntityTitleComponent} from "../entity-title/entity-title.component";
|
||||
import {FormsModule} from "@angular/forms";
|
||||
import {ImageComponent} from "../../shared/image/image.component";
|
||||
import {DownloadEvent, DownloadService} from "../../shared/_services/download.service";
|
||||
|
|
@ -39,7 +37,6 @@ import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
|||
import {map} from "rxjs/operators";
|
||||
import {AccountService} from "../../_services/account.service";
|
||||
import {BulkSelectionService} from "../bulk-selection.service";
|
||||
import {User} from "../../_models/user";
|
||||
import {ScrollService} from "../../_services/scroll.service";
|
||||
import {ReaderService} from "../../_services/reader.service";
|
||||
import {SeriesFormatComponent} from "../../shared/series-format/series-format.component";
|
||||
|
|
@ -147,8 +144,6 @@ export class SeriesCardComponent implements OnInit, OnChanges {
|
|||
*/
|
||||
prevOffset: number = 0;
|
||||
selectionInProgress: boolean = false;
|
||||
private user: User | undefined;
|
||||
|
||||
|
||||
@HostListener('touchmove', ['$event'])
|
||||
onTouchMove(event: TouchEvent) {
|
||||
|
|
@ -192,15 +187,15 @@ export class SeriesCardComponent implements OnInit, OnChanges {
|
|||
|
||||
ngOnChanges(changes: any) {
|
||||
if (this.series) {
|
||||
this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(user => {
|
||||
this.user = user;
|
||||
});
|
||||
// this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(user => {
|
||||
// this.user = user;
|
||||
// });
|
||||
|
||||
this.download$ = this.downloadService.activeDownloads$.pipe(takeUntilDestroyed(this.destroyRef), map((events) => {
|
||||
return this.downloadService.mapToEntityType(events, this.series);
|
||||
}));
|
||||
|
||||
this.actions = [...this.actionFactoryService.getSeriesActions((action: ActionItem<Series>, series: Series) => this.handleSeriesActionCallback(action, series))];
|
||||
this.actions = [...this.actionFactoryService.getSeriesActions(this.handleSeriesActionCallback.bind(this))];
|
||||
if (this.isOnDeck) {
|
||||
const othersIndex = this.actions.findIndex(obj => obj.title === 'others');
|
||||
const othersAction = deepClone(this.actions[othersIndex]) as ActionItem<Series>;
|
||||
|
|
@ -209,9 +204,11 @@ export class SeriesCardComponent implements OnInit, OnChanges {
|
|||
action: Action.RemoveFromOnDeck,
|
||||
title: 'remove-from-on-deck',
|
||||
description: '',
|
||||
callback: (action: ActionItem<Series>, series: Series) => this.handleSeriesActionCallback(action, series),
|
||||
callback: this.handleSeriesActionCallback.bind(this),
|
||||
class: 'danger',
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
shouldRender: (_, _2, _3) => true,
|
||||
children: [],
|
||||
});
|
||||
this.actions[othersIndex] = othersAction;
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@
|
|||
|
||||
@if (actions && actions.length > 0) {
|
||||
<span class="card-actions">
|
||||
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="volume.name"></app-card-actionables>
|
||||
<app-card-actionables [entity]="volume" [inputActions]="actions" [labelBy]="volume.name"></app-card-actionables>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import {DownloadEvent, DownloadService} from "../../shared/_services/download.se
|
|||
import {EVENTS, MessageHubService} from "../../_services/message-hub.service";
|
||||
import {AccountService} from "../../_services/account.service";
|
||||
import {ScrollService} from "../../_services/scroll.service";
|
||||
import {Action, ActionItem} from "../../_services/action-factory.service";
|
||||
import {ActionItem} from "../../_services/action-factory.service";
|
||||
import {ReaderService} from "../../_services/reader.service";
|
||||
import {Observable} from "rxjs";
|
||||
import {User} from "../../_models/user";
|
||||
|
|
@ -33,7 +33,6 @@ import {UserProgressUpdateEvent} from "../../_models/events/user-progress-update
|
|||
import {Volume} from "../../_models/volume";
|
||||
import {UtilityService} from "../../shared/_services/utility.service";
|
||||
import {LibraryType} from "../../_models/library/library";
|
||||
import {Device} from "../../_models/device/device";
|
||||
import {ActionService} from "../../_services/action.service";
|
||||
import {FormsModule} from "@angular/forms";
|
||||
|
||||
|
|
@ -143,8 +142,6 @@ export class VolumeCardComponent implements OnInit {
|
|||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.filterSendTo();
|
||||
|
||||
this.accountService.currentUser$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(user => {
|
||||
this.user = user;
|
||||
});
|
||||
|
|
@ -180,30 +177,6 @@ export class VolumeCardComponent implements OnInit {
|
|||
this.cdRef.detectChanges();
|
||||
}
|
||||
|
||||
|
||||
filterSendTo() {
|
||||
if (!this.actions || this.actions.length === 0) return;
|
||||
// TODO: See if we can handle send to for volumes
|
||||
//this.actions = this.actionFactoryService.filterSendToAction(this.actions, this.volume);
|
||||
}
|
||||
|
||||
performAction(action: ActionItem<Volume>) {
|
||||
if (action.action == Action.Download) {
|
||||
this.downloadService.download('volume', this.volume);
|
||||
return; // Don't propagate the download from a card
|
||||
}
|
||||
|
||||
if (action.action == Action.SendTo) {
|
||||
const device = (action._extra!.data as Device);
|
||||
this.actionService.sendToDevice(this.volume.chapters.map(c => c.id), device);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof action.callback === 'function') {
|
||||
action.callback(action, this.volume);
|
||||
}
|
||||
}
|
||||
|
||||
handleClick(event: any) {
|
||||
if (this.bulkSelectionService.hasSelections()) {
|
||||
this.handleSelection(event);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<div class="carousel-container mb-3">
|
||||
<div>
|
||||
@if (actionables.length > 0) {
|
||||
<app-card-actionables [actions]="actionables" (actionHandler)="performAction($event)"></app-card-actionables>
|
||||
<app-card-actionables [inputActions]="actionables" (actionHandler)="performAction($event)"></app-card-actionables>
|
||||
}
|
||||
<h4 class="header" (click)="sectionClicked($event)" [ngClass]="{'non-selectable': !clickableTitle}">
|
||||
@if (titleLink !== '') {
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@
|
|||
|
||||
<div class="col-auto ms-2k">
|
||||
<div class="card-actions" [ngbTooltip]="t('more-alt')">
|
||||
<app-card-actionables (actionHandler)="performAction($event)" [actions]="chapterActions" [labelBy]="series.name + ' ' + chapter.minNumber" iconClass="fa-ellipsis-h" btnClass="btn-actions btn"></app-card-actionables>
|
||||
<app-card-actionables [entity]="chapter" [inputActions]="chapterActions" [labelBy]="series.name + ' ' + chapter.minNumber" iconClass="fa-ellipsis-h" btnClass="btn-actions btn"></app-card-actionables>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ enum TabID {
|
|||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-chapter-detail',
|
||||
selector: 'app-chapter-detail',
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
CardActionablesComponent,
|
||||
|
|
@ -116,9 +116,9 @@ enum TabID {
|
|||
ReviewsComponent,
|
||||
ExternalRatingComponent
|
||||
],
|
||||
templateUrl: './chapter-detail.component.html',
|
||||
styleUrl: './chapter-detail.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
templateUrl: './chapter-detail.component.html',
|
||||
styleUrl: './chapter-detail.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ChapterDetailComponent implements OnInit {
|
||||
|
||||
|
|
@ -279,7 +279,11 @@ export class ChapterDetailComponent implements OnInit {
|
|||
|
||||
this.showDetailsTab = hasAnyCast(this.chapter) || (this.chapter.genres || []).length > 0 ||
|
||||
(this.chapter.tags || []).length > 0 || this.chapter.webLinks.length > 0;
|
||||
|
||||
|
||||
if (!this.showDetailsTab && this.activeTabId === TabID.Details) {
|
||||
this.activeTabId = TabID.Reviews;
|
||||
}
|
||||
|
||||
this.isLoading = false;
|
||||
this.cdRef.markForCheck();
|
||||
});
|
||||
|
|
@ -335,10 +339,6 @@ export class ChapterDetailComponent implements OnInit {
|
|||
this.location.replaceState(newUrl)
|
||||
}
|
||||
|
||||
openPerson(field: FilterField, value: number) {
|
||||
this.filterUtilityService.applyFilter(['all-series'], field, FilterComparison.Equal, `${value}`).subscribe();
|
||||
}
|
||||
|
||||
downloadChapter() {
|
||||
if (this.downloadInProgress) return;
|
||||
this.downloadService.download('chapter', this.chapter!, (d) => {
|
||||
|
|
@ -356,11 +356,6 @@ export class ChapterDetailComponent implements OnInit {
|
|||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
performAction(action: ActionItem<Chapter>) {
|
||||
if (typeof action.callback === 'function') {
|
||||
action.callback(action, this.chapter!);
|
||||
}
|
||||
}
|
||||
|
||||
handleChapterActionCallback(action: ActionItem<Chapter>, chapter: Chapter) {
|
||||
switch (action.action) {
|
||||
|
|
|
|||
|
|
@ -6,11 +6,9 @@
|
|||
<ng-container title>
|
||||
@if (collectionTag) {
|
||||
<h4>
|
||||
{{collectionTag.title}}
|
||||
@if(collectionTag.promoted) {
|
||||
<span class="ms-1">(<i aria-hidden="true" class="fa fa-angle-double-up"></i>)</span>
|
||||
}
|
||||
<app-card-actionables [disabled]="actionInProgress" (actionHandler)="performAction($event)" [actions]="collectionTagActions" [labelBy]="collectionTag.title" iconClass="fa-ellipsis-v"></app-card-actionables>
|
||||
<app-promoted-icon [promoted]="collectionTag.promoted"></app-promoted-icon>
|
||||
<span class="ms-2">{{collectionTag.title}}</span>
|
||||
<app-card-actionables [entity]="collectionTag" [disabled]="actionInProgress" [inputActions]="collectionTagActions" [labelBy]="collectionTag.title" iconClass="fa-ellipsis-v"></app-card-actionables>
|
||||
</h4>
|
||||
}
|
||||
<h5 subtitle class="subtitle-with-actionables">{{t('item-count', {num: series.length})}}</h5>
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ import {
|
|||
} from "../../../_single-module/smart-collection-drawer/smart-collection-drawer.component";
|
||||
import {DefaultModalOptions} from "../../../_models/default-modal-options";
|
||||
import {ScrobbleProviderNamePipe} from "../../../_pipes/scrobble-provider-name.pipe";
|
||||
import {PromotedIconComponent} from "../../../shared/_components/promoted-icon/promoted-icon.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-collection-detail',
|
||||
|
|
@ -69,7 +70,7 @@ import {ScrobbleProviderNamePipe} from "../../../_pipes/scrobble-provider-name.p
|
|||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [SideNavCompanionBarComponent, CardActionablesComponent, ImageComponent, ReadMoreComponent,
|
||||
BulkOperationsComponent, CardDetailLayoutComponent, SeriesCardComponent, TranslocoDirective, NgbTooltip,
|
||||
DatePipe, DefaultDatePipe, ProviderImagePipe, AsyncPipe, ScrobbleProviderNamePipe]
|
||||
DatePipe, DefaultDatePipe, ProviderImagePipe, AsyncPipe, ScrobbleProviderNamePipe, PromotedIconComponent]
|
||||
})
|
||||
export class CollectionDetailComponent implements OnInit, AfterContentChecked {
|
||||
|
||||
|
|
@ -304,12 +305,6 @@ export class CollectionDetailComponent implements OnInit, AfterContentChecked {
|
|||
}
|
||||
}
|
||||
|
||||
performAction(action: ActionItem<any>) {
|
||||
if (typeof action.callback === 'function') {
|
||||
action.callback(action, this.collectionTag);
|
||||
}
|
||||
}
|
||||
|
||||
openEditCollectionTagModal(collectionTag: UserCollection) {
|
||||
const modalRef = this.modalService.open(EditCollectionTagsComponent, DefaultModalOptions);
|
||||
modalRef.componentInstance.tag = this.collectionTag;
|
||||
|
|
@ -320,7 +315,6 @@ export class CollectionDetailComponent implements OnInit, AfterContentChecked {
|
|||
}
|
||||
|
||||
openSyncDetailDrawer() {
|
||||
|
||||
const ref = this.offcanvasService.open(SmartCollectionDrawerComponent, {position: 'end', panelClass: ''});
|
||||
ref.componentInstance.collection = this.collectionTag;
|
||||
ref.componentInstance.series = this.series;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<app-side-nav-companion-bar [hasFilter]="true" (filterOpen)="filterOpen.emit($event)" [filterActive]="filterActive">
|
||||
<h4 title>
|
||||
<span>{{libraryName}}</span>
|
||||
<app-card-actionables [actions]="actions" (actionHandler)="performAction($event)"></app-card-actionables>
|
||||
<app-card-actionables [inputActions]="actions" (actionHandler)="performAction($event)"></app-card-actionables>
|
||||
</h4>
|
||||
@if (active.fragment === '') {
|
||||
<h5 subtitle class="subtitle-with-actionables">{{t('common.series-count', {num: pagination.totalItems | number})}} </h5>
|
||||
|
|
@ -31,7 +31,6 @@
|
|||
</ng-template>
|
||||
|
||||
<ng-template #noData>
|
||||
<!-- TODO: Come back and figure this out -->
|
||||
{{t('common.no-data')}}
|
||||
</ng-template>
|
||||
</app-card-detail-layout>
|
||||
|
|
|
|||
|
|
@ -297,8 +297,6 @@ export class LibraryDetailComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
performAction(action: ActionItem<any>) {
|
||||
if (typeof action.callback === 'function') {
|
||||
action.callback(action, undefined);
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@
|
|||
} @else {
|
||||
<button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="resetField()"></button>
|
||||
}
|
||||
} @else {
|
||||
<div class="input-hint">
|
||||
Ctrl+K
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,17 @@
|
|||
right: 5px;
|
||||
}
|
||||
|
||||
.input-hint {
|
||||
font-size: 0.8rem;
|
||||
margin-top: 3px;
|
||||
margin-bottom: 3px;
|
||||
margin-right: 9px;
|
||||
border: 1px solid var(--input-hint-border-color, lightgrey);
|
||||
color: var(--input-hint-text-color);
|
||||
border-radius: 4px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
|
||||
.typeahead-input {
|
||||
border: 1px solid transparent;
|
||||
|
|
|
|||
|
|
@ -100,7 +100,9 @@ export class GroupedTypeaheadComponent implements OnInit {
|
|||
|
||||
|
||||
hasFocus: boolean = false;
|
||||
typeaheadForm: FormGroup = new FormGroup({});
|
||||
typeaheadForm: FormGroup = new FormGroup({
|
||||
typeahead: new FormControl('', []),
|
||||
});
|
||||
includeChapterAndFiles: boolean = false;
|
||||
prevSearchTerm: string = '';
|
||||
searchSettingsForm = new FormGroup(({'includeExtras': new FormControl(false)}));
|
||||
|
|
@ -121,22 +123,37 @@ export class GroupedTypeaheadComponent implements OnInit {
|
|||
this.close();
|
||||
}
|
||||
|
||||
@HostListener('window:keydown', ['$event'])
|
||||
@HostListener('document:keydown', ['$event'])
|
||||
handleKeyPress(event: KeyboardEvent) {
|
||||
if (!this.hasFocus) { return; }
|
||||
|
||||
const isCtrlOrMeta = event.ctrlKey || event.metaKey;
|
||||
|
||||
|
||||
switch(event.key) {
|
||||
case KEY_CODES.ESC_KEY:
|
||||
if (!this.hasFocus) { return; }
|
||||
this.close();
|
||||
event.stopPropagation();
|
||||
break;
|
||||
|
||||
case KEY_CODES.K:
|
||||
if (isCtrlOrMeta) {
|
||||
if (this.inputElem.nativeElement) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
this.inputElem.nativeElement.focus();
|
||||
this.inputElem.nativeElement.click();
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.typeaheadForm.addControl('typeahead', new FormControl(this.initialValue, []));
|
||||
this.typeaheadForm.get('typeahead')?.setValue(this.initialValue);
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
this.searchSettingsForm.get('includeExtras')!.valueChanges.pipe(
|
||||
|
|
|
|||
|
|
@ -118,7 +118,14 @@
|
|||
width="24px" [imageUrl]="imageService.getPersonImage(item.id)" [errorImage]="imageService.noPersonImage"></app-image>
|
||||
</div>
|
||||
<div class="ms-1">
|
||||
<div>{{item.name}}</div>
|
||||
<div>
|
||||
{{item.name}}
|
||||
</div>
|
||||
@if (item.aliases.length > 0) {
|
||||
<span class="small-text">
|
||||
{{t('person-aka-status')}}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
|
@ -206,7 +213,7 @@
|
|||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</nav>
|
||||
|
|
|
|||
|
|
@ -149,3 +149,7 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.small-text {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,6 +96,19 @@
|
|||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="TabID.Aliases">
|
||||
<a ngbNavLink>{{t(TabID.Aliases)}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<h5>{{t('aliases-label')}}</h5>
|
||||
<div class="text-muted mb-2">{{t('aliases-tooltip')}}</div>
|
||||
<app-edit-list [items]="person.aliases"
|
||||
[asyncValidators]="[aliasValidator()]"
|
||||
(updateItems)="person.aliases = $event"
|
||||
[errorMessage]="t('alias-overlap')"
|
||||
[label]="t('aliases-label')"/>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
|
||||
<li [ngbNavItem]="TabID.CoverImage">
|
||||
<a ngbNavLink>{{t(TabID.CoverImage)}}</a>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input, OnInit} from '@angular/core';
|
||||
import {Breakpoint, UtilityService} from "../../../shared/_services/utility.service";
|
||||
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms";
|
||||
import {
|
||||
AbstractControl,
|
||||
AsyncValidatorFn,
|
||||
FormControl,
|
||||
FormGroup,
|
||||
ReactiveFormsModule,
|
||||
ValidationErrors,
|
||||
Validators
|
||||
} from "@angular/forms";
|
||||
import {Person} from "../../../_models/metadata/person";
|
||||
import {
|
||||
NgbActiveModal,
|
||||
|
|
@ -14,14 +22,16 @@ import {
|
|||
import {PersonService} from "../../../_services/person.service";
|
||||
import {translate, TranslocoDirective} from '@jsverse/transloco';
|
||||
import {CoverImageChooserComponent} from "../../../cards/cover-image-chooser/cover-image-chooser.component";
|
||||
import {forkJoin} from "rxjs";
|
||||
import {forkJoin, map, of} from "rxjs";
|
||||
import {UploadService} from "../../../_services/upload.service";
|
||||
import {SettingItemComponent} from "../../../settings/_components/setting-item/setting-item.component";
|
||||
import {AccountService} from "../../../_services/account.service";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {EditListComponent} from "../../../shared/edit-list/edit-list.component";
|
||||
|
||||
enum TabID {
|
||||
General = 'general-tab',
|
||||
Aliases = 'aliases-tab',
|
||||
CoverImage = 'cover-image-tab',
|
||||
}
|
||||
|
||||
|
|
@ -37,7 +47,8 @@ enum TabID {
|
|||
NgbNavOutlet,
|
||||
CoverImageChooserComponent,
|
||||
SettingItemComponent,
|
||||
NgbNavLink
|
||||
NgbNavLink,
|
||||
EditListComponent
|
||||
],
|
||||
templateUrl: './edit-person-modal.component.html',
|
||||
styleUrl: './edit-person-modal.component.scss',
|
||||
|
|
@ -117,6 +128,7 @@ export class EditPersonModalComponent implements OnInit {
|
|||
// @ts-ignore
|
||||
malId: this.editForm.get('malId')!.value === '' ? null : parseInt(this.editForm.get('malId').value, 10),
|
||||
hardcoverId: this.editForm.get('hardcoverId')!.value || '',
|
||||
aliases: this.person.aliases,
|
||||
};
|
||||
apis.push(this.personService.updatePerson(person));
|
||||
|
||||
|
|
@ -165,4 +177,21 @@ export class EditPersonModalComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
aliasValidator(): AsyncValidatorFn {
|
||||
return (control: AbstractControl) => {
|
||||
const name = control.value;
|
||||
if (!name || name.trim().length === 0) {
|
||||
return of(null);
|
||||
}
|
||||
|
||||
return this.personService.isValidAlias(this.person.id, name).pipe(map(valid => {
|
||||
if (valid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { 'invalidAlias': {'alias': name} } as ValidationErrors;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
<ng-container *transloco="let t; prefix:'merge-person-modal'">
|
||||
<div class="modal-container">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">
|
||||
{{t('title', {personName: this.person.name})}}</h4>
|
||||
<button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="close()"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-body scrollable-modal d-flex flex-column" style="min-height: 300px;" >
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 pe-2">
|
||||
<div class="mb-3">
|
||||
|
||||
<app-setting-item [title]="t('src')" [subtitle]="t('merge-warning')" [toggleOnViewClick]="false" [showEdit]="false">
|
||||
<ng-template #view>
|
||||
<app-typeahead [settings]="typeAheadSettings" (selectedData)="updatePerson($event)" [unFocus]="typeAheadUnfocus">
|
||||
<ng-template #badgeItem let-item let-position="idx">
|
||||
{{item.name}}
|
||||
</ng-template>
|
||||
<ng-template #optionItem let-item let-position="idx">
|
||||
{{item.name}}
|
||||
</ng-template>
|
||||
</app-typeahead>
|
||||
</ng-template>
|
||||
</app-setting-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (mergee) {
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 pe-2">
|
||||
<div class="mb-3">
|
||||
|
||||
<h5>{{t('alias-title')}}</h5>
|
||||
|
||||
<app-badge-expander [items]="allNewAliases()">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx" let-last="last">
|
||||
{{item}}
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
|
||||
@if (knownFor$ | async; as knownFor) {
|
||||
<h5 class="mt-2">{{t('known-for-title')}}</h5>
|
||||
|
||||
<app-badge-expander [items]="knownFor">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx" let-last="last">
|
||||
{{item.name}}
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" (click)="close()">{{t('close')}}</button>
|
||||
<button type="submit" class="btn btn-primary" (click)="save()" [disabled]="mergee === null" >{{t('save')}}</button>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
import {Component, DestroyRef, EventEmitter, inject, Input, OnInit} from '@angular/core';
|
||||
import {Person} from "../../../_models/metadata/person";
|
||||
import {PersonService} from "../../../_services/person.service";
|
||||
import {NgbActiveModal} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {TranslocoDirective} from "@jsverse/transloco";
|
||||
import {TypeaheadComponent} from "../../../typeahead/_components/typeahead.component";
|
||||
import {TypeaheadSettings} from "../../../typeahead/_models/typeahead-settings";
|
||||
import {map} from "rxjs/operators";
|
||||
import {UtilityService} from "../../../shared/_services/utility.service";
|
||||
import {SettingItemComponent} from "../../../settings/_components/setting-item/setting-item.component";
|
||||
import {BadgeExpanderComponent} from "../../../shared/badge-expander/badge-expander.component";
|
||||
import {FilterField} from "../../../_models/metadata/v2/filter-field";
|
||||
import {Observable, of} from "rxjs";
|
||||
import {Series} from "../../../_models/series";
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import {AsyncPipe} from "@angular/common";
|
||||
|
||||
@Component({
|
||||
selector: 'app-merge-person-modal',
|
||||
imports: [
|
||||
TranslocoDirective,
|
||||
TypeaheadComponent,
|
||||
SettingItemComponent,
|
||||
BadgeExpanderComponent,
|
||||
AsyncPipe
|
||||
],
|
||||
templateUrl: './merge-person-modal.component.html',
|
||||
styleUrl: './merge-person-modal.component.scss'
|
||||
})
|
||||
export class MergePersonModalComponent implements OnInit {
|
||||
|
||||
private readonly personService = inject(PersonService);
|
||||
public readonly utilityService = inject(UtilityService);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly modal = inject(NgbActiveModal);
|
||||
protected readonly toastr = inject(ToastrService);
|
||||
|
||||
typeAheadSettings!: TypeaheadSettings<Person>;
|
||||
typeAheadUnfocus = new EventEmitter<string>();
|
||||
|
||||
@Input({required: true}) person!: Person;
|
||||
|
||||
mergee: Person | null = null;
|
||||
knownFor$: Observable<Series[]> | null = null;
|
||||
|
||||
save() {
|
||||
if (!this.mergee) {
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
|
||||
this.personService.mergePerson(this.person.id, this.mergee.id).subscribe(person => {
|
||||
this.modal.close({success: true, person: person});
|
||||
})
|
||||
}
|
||||
|
||||
close() {
|
||||
this.modal.close({success: false, person: this.person});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.typeAheadSettings = new TypeaheadSettings<Person>();
|
||||
this.typeAheadSettings.minCharacters = 0;
|
||||
this.typeAheadSettings.multiple = false;
|
||||
this.typeAheadSettings.addIfNonExisting = false;
|
||||
this.typeAheadSettings.id = "merge-person-modal-typeahead";
|
||||
this.typeAheadSettings.compareFn = (options: Person[], filter: string) => {
|
||||
return options.filter(m => this.utilityService.filter(m.name, filter));
|
||||
}
|
||||
this.typeAheadSettings.selectionCompareFn = (a: Person, b: Person) => {
|
||||
return a.name == b.name;
|
||||
}
|
||||
this.typeAheadSettings.fetchFn = (filter: string) => {
|
||||
if (filter.length == 0) return of([]);
|
||||
|
||||
return this.personService.searchPerson(filter).pipe(map(people => {
|
||||
return people.filter(p => this.utilityService.filter(p.name, filter) && p.id != this.person.id);
|
||||
}));
|
||||
};
|
||||
|
||||
this.typeAheadSettings.trackByIdentityFn = (index, value) => `${value.name}_${value.id}`;
|
||||
}
|
||||
|
||||
updatePerson(people: Person[]) {
|
||||
if (people.length == 0) return;
|
||||
|
||||
this.typeAheadUnfocus.emit(this.typeAheadSettings.id);
|
||||
this.mergee = people[0];
|
||||
this.knownFor$ = this.personService.getSeriesMostKnownFor(this.mergee.id)
|
||||
.pipe(takeUntilDestroyed(this.destroyRef));
|
||||
}
|
||||
|
||||
protected readonly FilterField = FilterField;
|
||||
|
||||
allNewAliases() {
|
||||
if (!this.mergee) return [];
|
||||
|
||||
return [this.mergee.name, ...this.mergee.aliases]
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
<app-side-nav-companion-bar>
|
||||
<ng-container title>
|
||||
<h2 class="title text-break">
|
||||
<app-card-actionables (actionHandler)="performAction($event)" [actions]="personActions" [labelBy]="person.name" iconClass="fa-ellipsis-v"></app-card-actionables>
|
||||
<app-card-actionables [entity]="person" [inputActions]="personActions" [labelBy]="person.name" iconClass="fa-ellipsis-v"></app-card-actionables>
|
||||
<span>{{person.name}}</span>
|
||||
|
||||
@if (person.aniListId) {
|
||||
|
|
@ -43,15 +43,43 @@
|
|||
|
||||
<div class="col-xl-10 col-lg-7 col-md-12 col-xs-12 col-sm-12 mt-2">
|
||||
<div class="row g-0 mt-2">
|
||||
<app-read-more [text]="person.description || defaultSummaryText"></app-read-more>
|
||||
<app-read-more [maxLength]="500" [text]="person.description || t('no-info')"></app-read-more>
|
||||
|
||||
|
||||
@if (person.aliases.length > 0) {
|
||||
<span class="fw-bold mt-2">{{t('aka-title')}}</span>
|
||||
<div>
|
||||
<app-badge-expander [items]="person.aliases"
|
||||
[itemsTillExpander]="6">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx" let-last="last">
|
||||
<span>{{item}}</span>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
@if (roles$ | async; as roles) {
|
||||
<div class="mt-1">
|
||||
<h5>{{t('all-roles')}}</h5>
|
||||
@for(role of roles; track role) {
|
||||
<app-tag-badge [selectionMode]="TagBadgeCursor.Clickable" (click)="loadFilterByRole(role)">{{role | personRole}}</app-tag-badge>
|
||||
}
|
||||
</div>
|
||||
@if (roles.length > 0) {
|
||||
<span class="fw-bold mt-2">{{t('all-roles')}}</span>
|
||||
<div>
|
||||
<app-badge-expander [items]="roles"
|
||||
[itemsTillExpander]="6">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx" let-last="last">
|
||||
<a href="javascript:void(0)" class="dark-exempt btn-icon" (click)="loadFilterByRole(item)">{{item | personRole}}</a>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
<!-- -->
|
||||
<!-- <div class="mt-1">-->
|
||||
<!-- <h5>{{t('all-roles')}}</h5>-->
|
||||
<!-- @for(role of roles; track role) {-->
|
||||
<!-- <app-tag-badge [selectionMode]="TagBadgeCursor.Clickable" (click)="loadFilterByRole(role)">{{role | personRole}}</app-tag-badge>-->
|
||||
<!-- }-->
|
||||
<!-- </div>-->
|
||||
}
|
||||
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,31 +1,30 @@
|
|||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component, DestroyRef,
|
||||
Component,
|
||||
DestroyRef,
|
||||
ElementRef,
|
||||
Inject,
|
||||
inject, OnInit,
|
||||
inject,
|
||||
OnInit,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import {ActivatedRoute, Router} from "@angular/router";
|
||||
import {PersonService} from "../_services/person.service";
|
||||
import {BehaviorSubject, EMPTY, Observable, switchMap, tap} from "rxjs";
|
||||
import {Person, PersonRole} from "../_models/metadata/person";
|
||||
import {AsyncPipe, NgStyle} from "@angular/common";
|
||||
import {AsyncPipe} from "@angular/common";
|
||||
import {ImageComponent} from "../shared/image/image.component";
|
||||
import {ImageService} from "../_services/image.service";
|
||||
import {
|
||||
SideNavCompanionBarComponent
|
||||
} from "../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component";
|
||||
import {ReadMoreComponent} from "../shared/read-more/read-more.component";
|
||||
import {TagBadgeComponent, TagBadgeCursor} from "../shared/tag-badge/tag-badge.component";
|
||||
import {PersonRolePipe} from "../_pipes/person-role.pipe";
|
||||
import {CarouselReelComponent} from "../carousel/_components/carousel-reel/carousel-reel.component";
|
||||
import {SeriesCardComponent} from "../cards/series-card/series-card.component";
|
||||
import {FilterComparison} from "../_models/metadata/v2/filter-comparison";
|
||||
import {FilterUtilitiesService} from "../shared/_services/filter-utilities.service";
|
||||
import {SeriesFilterV2} from "../_models/metadata/v2/series-filter-v2";
|
||||
import {allPeople, personRoleForFilterField} from "../_models/metadata/v2/filter-field";
|
||||
import {allPeople, FilterField, personRoleForFilterField} from "../_models/metadata/v2/filter-field";
|
||||
import {Series} from "../_models/series";
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import {FilterCombination} from "../_models/metadata/v2/filter-combination";
|
||||
|
|
@ -42,28 +41,38 @@ import {DefaultModalOptions} from "../_models/default-modal-options";
|
|||
import {ToastrService} from "ngx-toastr";
|
||||
import {LicenseService} from "../_services/license.service";
|
||||
import {SafeUrlPipe} from "../_pipes/safe-url.pipe";
|
||||
import {MergePersonModalComponent} from "./_modal/merge-person-modal/merge-person-modal.component";
|
||||
import {EVENTS, MessageHubService} from "../_services/message-hub.service";
|
||||
import {BadgeExpanderComponent} from "../shared/badge-expander/badge-expander.component";
|
||||
|
||||
interface PersonMergeEvent {
|
||||
srcId: number,
|
||||
dstId: number,
|
||||
dstName: number,
|
||||
}
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-person-detail',
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
ImageComponent,
|
||||
SideNavCompanionBarComponent,
|
||||
ReadMoreComponent,
|
||||
TagBadgeComponent,
|
||||
PersonRolePipe,
|
||||
CarouselReelComponent,
|
||||
CardItemComponent,
|
||||
CardActionablesComponent,
|
||||
TranslocoDirective,
|
||||
ChapterCardComponent,
|
||||
SafeUrlPipe
|
||||
],
|
||||
templateUrl: './person-detail.component.html',
|
||||
styleUrl: './person-detail.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
selector: 'app-person-detail',
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
ImageComponent,
|
||||
SideNavCompanionBarComponent,
|
||||
ReadMoreComponent,
|
||||
PersonRolePipe,
|
||||
CarouselReelComponent,
|
||||
CardItemComponent,
|
||||
CardActionablesComponent,
|
||||
TranslocoDirective,
|
||||
ChapterCardComponent,
|
||||
SafeUrlPipe,
|
||||
BadgeExpanderComponent
|
||||
],
|
||||
templateUrl: './person-detail.component.html',
|
||||
styleUrl: './person-detail.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class PersonDetailComponent {
|
||||
export class PersonDetailComponent implements OnInit {
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private readonly router = inject(Router);
|
||||
private readonly filterUtilityService = inject(FilterUtilitiesService);
|
||||
|
|
@ -77,8 +86,9 @@ export class PersonDetailComponent {
|
|||
protected readonly licenseService = inject(LicenseService);
|
||||
private readonly themeService = inject(ThemeService);
|
||||
private readonly toastr = inject(ToastrService);
|
||||
private readonly messageHubService = inject(MessageHubService)
|
||||
|
||||
protected readonly TagBadgeCursor = TagBadgeCursor;
|
||||
protected readonly FilterField = FilterField;
|
||||
|
||||
@ViewChild('scrollingBlock') scrollingBlock: ElementRef<HTMLDivElement> | undefined;
|
||||
@ViewChild('companionBar') companionBar: ElementRef<HTMLDivElement> | undefined;
|
||||
|
|
@ -88,11 +98,11 @@ export class PersonDetailComponent {
|
|||
roles$: Observable<PersonRole[]> | null = null;
|
||||
roles: PersonRole[] | null = null;
|
||||
works$: Observable<Series[]> | null = null;
|
||||
defaultSummaryText = 'No information about this Person';
|
||||
filter: SeriesFilterV2 | null = null;
|
||||
personActions: Array<ActionItem<Person>> = this.actionService.getPersonActions(this.handleAction.bind(this));
|
||||
chaptersByRole: any = {};
|
||||
anilistUrl: string = '';
|
||||
|
||||
private readonly personSubject = new BehaviorSubject<Person | null>(null);
|
||||
protected readonly person$ = this.personSubject.asObservable().pipe(tap(p => {
|
||||
if (p?.aniListId) {
|
||||
|
|
@ -118,43 +128,58 @@ export class PersonDetailComponent {
|
|||
return this.personService.get(personName);
|
||||
}),
|
||||
tap((person) => {
|
||||
|
||||
if (person == null) {
|
||||
this.toastr.error(translate('toasts.unauthorized-1'));
|
||||
this.router.navigateByUrl('/home');
|
||||
return;
|
||||
}
|
||||
|
||||
this.person = person;
|
||||
this.personSubject.next(person); // emit the person data for subscribers
|
||||
this.themeService.setColorScape(person.primaryColor || '', person.secondaryColor);
|
||||
|
||||
// Fetch roles and process them
|
||||
this.roles$ = this.personService.getRolesForPerson(this.person.id).pipe(
|
||||
tap(roles => {
|
||||
this.roles = roles;
|
||||
this.filter = this.createFilter(roles);
|
||||
this.chaptersByRole = {}; // Reset chaptersByRole for each person
|
||||
|
||||
// Populate chapters by role
|
||||
roles.forEach(role => {
|
||||
this.chaptersByRole[role] = this.personService.getChaptersByRole(person.id, role)
|
||||
.pipe(takeUntilDestroyed(this.destroyRef));
|
||||
});
|
||||
this.cdRef.markForCheck();
|
||||
}),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
);
|
||||
|
||||
// Fetch series known for this person
|
||||
this.works$ = this.personService.getSeriesMostKnownFor(person.id).pipe(
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
);
|
||||
this.setPerson(person);
|
||||
}),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
).subscribe();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.messageHubService.messages$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(message => {
|
||||
if (message.event !== EVENTS.PersonMerged) return;
|
||||
|
||||
const event = message.payload as PersonMergeEvent;
|
||||
if (event.srcId !== this.person?.id) return;
|
||||
|
||||
this.router.navigate(['person', event.dstName]);
|
||||
});
|
||||
}
|
||||
|
||||
private setPerson(person: Person) {
|
||||
this.person = person;
|
||||
this.personSubject.next(person); // emit the person data for subscribers
|
||||
this.themeService.setColorScape(person.primaryColor || '', person.secondaryColor);
|
||||
|
||||
// Fetch roles and process them
|
||||
this.roles$ = this.personService.getRolesForPerson(this.person.id).pipe(
|
||||
tap(roles => {
|
||||
this.roles = roles;
|
||||
this.filter = this.createFilter(roles);
|
||||
this.chaptersByRole = {}; // Reset chaptersByRole for each person
|
||||
|
||||
// Populate chapters by role
|
||||
roles.forEach(role => {
|
||||
this.chaptersByRole[role] = this.personService.getChaptersByRole(person.id, role)
|
||||
.pipe(takeUntilDestroyed(this.destroyRef));
|
||||
});
|
||||
this.cdRef.markForCheck();
|
||||
}),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
);
|
||||
|
||||
// Fetch series known for this person
|
||||
this.works$ = this.personService.getSeriesMostKnownFor(person.id).pipe(
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
createFilter(roles: PersonRole[]) {
|
||||
const filter: SeriesFilterV2 = this.filterUtilityService.createSeriesV2Filter();
|
||||
filter.combination = FilterCombination.Or;
|
||||
|
|
@ -229,14 +254,27 @@ export class PersonDetailComponent {
|
|||
}
|
||||
});
|
||||
break;
|
||||
case (Action.Merge):
|
||||
this.mergePersonAction();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
performAction(action: ActionItem<any>) {
|
||||
if (typeof action.callback === 'function') {
|
||||
action.callback(action, this.person);
|
||||
}
|
||||
private mergePersonAction() {
|
||||
const ref = this.modalService.open(MergePersonModalComponent, DefaultModalOptions);
|
||||
ref.componentInstance.person = this.person;
|
||||
|
||||
ref.closed.subscribe(r => {
|
||||
if (r.success) {
|
||||
// Reload the person data, as relations may have changed
|
||||
this.personService.get(r.person.name).subscribe(person => {
|
||||
this.setPerson(person!);
|
||||
this.cdRef.markForCheck();
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,12 +10,10 @@
|
|||
|
||||
<div class="col-xl-10 col-lg-7 col-md-12 col-sm-12 col-xs-12">
|
||||
<h4 class="title mb-2">
|
||||
<span>{{readingList.title}}
|
||||
@if (readingList.promoted) {
|
||||
(<app-promoted-icon [promoted]="readingList.promoted"></app-promoted-icon>)
|
||||
}
|
||||
|
||||
@if( isLoading) {
|
||||
<span>
|
||||
<app-promoted-icon [promoted]="readingList.promoted"></app-promoted-icon>
|
||||
<span class="ms-2">{{readingList.title}}</span>
|
||||
@if(isLoading) {
|
||||
<div class="spinner-border spinner-border-sm text-primary" role="status">
|
||||
<span class="visually-hidden">loading...</span>
|
||||
</div>
|
||||
|
|
@ -87,7 +85,7 @@
|
|||
|
||||
<div class="col-auto ms-2 d-none d-md-block">
|
||||
<div class="card-actions btn-actions" [ngbTooltip]="t('more-alt')">
|
||||
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="readingList.title" iconClass="fa-ellipsis-h" btnClass="btn"></app-card-actionables>
|
||||
<app-card-actionables [entity]="readingList" [inputActions]="actions" [labelBy]="readingList.title" iconClass="fa-ellipsis-h" btnClass="btn"></app-card-actionables>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -273,11 +273,6 @@ export class ReadingListDetailComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
performAction(action: ActionItem<any>) {
|
||||
if (typeof action.callback === 'function') {
|
||||
action.callback(action, this.readingList);
|
||||
}
|
||||
}
|
||||
|
||||
readChapter(item: ReadingListItem) {
|
||||
if (!this.readingList) return;
|
||||
|
|
@ -387,12 +382,6 @@ export class ReadingListDetailComponent implements OnInit {
|
|||
{queryParams: {readingListId: this.readingList.id, incognitoMode: incognitoMode}});
|
||||
}
|
||||
|
||||
updateAccessibilityMode() {
|
||||
this.accessibilityMode = !this.accessibilityMode;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
|
||||
toggleReorder() {
|
||||
this.formGroup.get('edit')?.setValue(!this.formGroup.get('edit')!.value);
|
||||
this.cdRef.markForCheck();
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<app-side-nav-companion-bar>
|
||||
<h4 title>
|
||||
<span>{{t('title')}}</span>
|
||||
<app-card-actionables [actions]="globalActions" (actionHandler)="performGlobalAction($event)"></app-card-actionables>
|
||||
<app-card-actionables [inputActions]="globalActions" (actionHandler)="performGlobalAction($event)"></app-card-actionables>
|
||||
</h4>
|
||||
@if (pagination) {
|
||||
<h5 subtitle class="subtitle-with-actionables">{{t('item-count', {num: pagination.totalItems | number})}}</h5>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
|
||||
</h4>
|
||||
<div class="subtitle mt-2 mb-2">
|
||||
@if (series.localizedName !== series.name) {
|
||||
@if (series.localizedName !== series.name && series.localizedName) {
|
||||
<span>{{series.localizedName | defaultValue}}</span>
|
||||
}
|
||||
</div>
|
||||
|
|
@ -96,7 +96,7 @@
|
|||
|
||||
<div class="col-auto ms-2">
|
||||
<div class="card-actions btn-actions" [ngbTooltip]="t('more-alt')">
|
||||
<app-card-actionables (actionHandler)="performAction($event)" [actions]="seriesActions" [labelBy]="series.name" iconClass="fa-ellipsis-h" btnClass="btn"></app-card-actionables>
|
||||
<app-card-actionables [entity]="series" [inputActions]="seriesActions" [labelBy]="series.name" iconClass="fa-ellipsis-h" btnClass="btn"></app-card-actionables>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -130,7 +130,7 @@
|
|||
<span class="fw-bold">{{t('publication-status-title')}}</span>
|
||||
<div>
|
||||
@if (seriesMetadata.publicationStatus | publicationStatus; as pubStatus) {
|
||||
<a class="dark-exempt btn-icon" (click)="openFilter(FilterField.PublicationStatus, seriesMetadata.publicationStatus)"
|
||||
<a class="dark-exempt btn-icon font-size" (click)="openFilter(FilterField.PublicationStatus, seriesMetadata!.publicationStatus)"
|
||||
href="javascript:void(0);"
|
||||
[ngbTooltip]="t('publication-status-tooltip') + (seriesMetadata.totalCount === 0 ? '' : ' (' + seriesMetadata.maxCount + ' / ' + seriesMetadata.totalCount + ')')">
|
||||
{{pubStatus}}
|
||||
|
|
|
|||
|
|
@ -30,3 +30,7 @@
|
|||
:host ::ng-deep .card-actions.btn-actions .btn {
|
||||
padding: 0.375rem 0.75rem;
|
||||
}
|
||||
|
||||
.font-size {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,11 +61,6 @@ import {ReaderService} from 'src/app/_services/reader.service';
|
|||
import {ReadingListService} from 'src/app/_services/reading-list.service';
|
||||
import {ScrollService} from 'src/app/_services/scroll.service';
|
||||
import {SeriesService} from 'src/app/_services/series.service';
|
||||
import {
|
||||
ReviewModalCloseAction,
|
||||
ReviewModalCloseEvent,
|
||||
ReviewModalComponent
|
||||
} from '../../../_single-module/review-modal/review-modal.component';
|
||||
import {PageLayoutMode} from 'src/app/_models/page-layout-mode';
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import {UserReview} from "../../../_single-module/review-card/user-review";
|
||||
|
|
@ -73,8 +68,6 @@ import {ExternalSeriesCardComponent} from '../../../cards/external-series-card/e
|
|||
import {SeriesCardComponent} from '../../../cards/series-card/series-card.component';
|
||||
import {VirtualScrollerModule} from '@iharbeck/ngx-virtual-scroller';
|
||||
import {BulkOperationsComponent} from '../../../cards/bulk-operations/bulk-operations.component';
|
||||
import {ReviewCardComponent} from '../../../_single-module/review-card/review-card.component';
|
||||
import {CarouselReelComponent} from '../../../carousel/_components/carousel-reel/carousel-reel.component';
|
||||
import {translate, TranslocoDirective, TranslocoService} from "@jsverse/transloco";
|
||||
import {CardActionablesComponent} from "../../../_single-module/card-actionables/card-actionables.component";
|
||||
import {PublicationStatus} from "../../../_models/metadata/publication-status";
|
||||
|
|
@ -1138,13 +1131,6 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
performAction(action: ActionItem<any>) {
|
||||
if (typeof action.callback === 'function') {
|
||||
action.callback(action, this.series);
|
||||
}
|
||||
}
|
||||
|
||||
downloadSeries() {
|
||||
this.downloadService.download('series', this.series, (d) => {
|
||||
this.downloadInProgress = !!d;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { HttpParams } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Chapter } from 'src/app/_models/chapter';
|
||||
import { LibraryType } from 'src/app/_models/library/library';
|
||||
import { MangaFormat } from 'src/app/_models/manga-format';
|
||||
import { PaginatedResult } from 'src/app/_models/pagination';
|
||||
import { Series } from 'src/app/_models/series';
|
||||
import { Volume } from 'src/app/_models/volume';
|
||||
import {translate, TranslocoService} from "@jsverse/transloco";
|
||||
import {HttpParams} from '@angular/common/http';
|
||||
import {Injectable} from '@angular/core';
|
||||
import {Chapter} from 'src/app/_models/chapter';
|
||||
import {LibraryType} from 'src/app/_models/library/library';
|
||||
import {MangaFormat} from 'src/app/_models/manga-format';
|
||||
import {PaginatedResult} from 'src/app/_models/pagination';
|
||||
import {Series} from 'src/app/_models/series';
|
||||
import {Volume} from 'src/app/_models/volume';
|
||||
import {translate} from "@jsverse/transloco";
|
||||
import {debounceTime, ReplaySubject, shareReplay} from "rxjs";
|
||||
|
||||
export enum KEY_CODES {
|
||||
|
|
@ -21,6 +21,7 @@ export enum KEY_CODES {
|
|||
B = 'b',
|
||||
F = 'f',
|
||||
H = 'h',
|
||||
K = 'k',
|
||||
BACKSPACE = 'Backspace',
|
||||
DELETE = 'Delete',
|
||||
SHIFT = 'Shift'
|
||||
|
|
@ -41,6 +42,9 @@ export class UtilityService {
|
|||
public readonly activeBreakpointSource = new ReplaySubject<Breakpoint>(1);
|
||||
public readonly activeBreakpoint$ = this.activeBreakpointSource.asObservable().pipe(debounceTime(60), shareReplay({bufferSize: 1, refCount: true}));
|
||||
|
||||
// TODO: I need an isPhone/Tablet so that I can easily trigger different views
|
||||
|
||||
|
||||
mangaFormatKeys: string[] = [];
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,4 +5,11 @@
|
|||
.collapsed {
|
||||
height: 35px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep .badge-expander .content {
|
||||
a,
|
||||
span {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<form [formGroup]="form" *transloco="let t">
|
||||
<div formArrayName="items">
|
||||
@for(item of ItemsArray.controls; let i = $index; track i) {
|
||||
<!-- We are tracking items, as the index will not always point towards the same item. -->
|
||||
@for(item of ItemsArray.controls; let i = $index; track item; let last = $last) {
|
||||
<div class="row g-0 mb-3">
|
||||
<div class="col-lg-10 col-md-12 pe-2">
|
||||
<div class="mb-3">
|
||||
|
|
@ -11,21 +12,30 @@
|
|||
[formControlName]="i"
|
||||
id="item--{{i}}"
|
||||
>
|
||||
@if (item.dirty && item.touched && errorMessage) {
|
||||
@if (item.status === "INVALID") {
|
||||
<div id="item--{{i}}-error" class="invalid-feedback" style="display: inline-block">
|
||||
{{errorMessage}}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<button class="btn btn-secondary me-1" (click)="add()">
|
||||
<i class="fa-solid fa-plus" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{t('common.add')}}</span>
|
||||
</button>
|
||||
<div class="col-lg-2">
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
class="btn btn-danger me-2"
|
||||
(click)="remove(i)"
|
||||
[disabled]="ItemsArray.length === 1"
|
||||
>
|
||||
<i class="fa-solid fa-xmark" aria-hidden="true"></i>
|
||||
<i class="fa-solid fa-trash" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{t('common.remove')}}</span>
|
||||
</button>
|
||||
@if (last){
|
||||
<button class="btn btn-secondary " (click)="add()">
|
||||
<i class="fa-solid fa-plus" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{t('common.add')}}</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
OnInit,
|
||||
Output
|
||||
} from '@angular/core';
|
||||
import {FormArray, FormControl, FormGroup, ReactiveFormsModule} from "@angular/forms";
|
||||
import {AsyncValidatorFn, FormArray, FormControl, FormGroup, ReactiveFormsModule, ValidatorFn} from "@angular/forms";
|
||||
import {TranslocoDirective} from "@jsverse/transloco";
|
||||
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
||||
import {debounceTime, distinctUntilChanged, tap} from "rxjs/operators";
|
||||
|
|
@ -28,6 +28,10 @@ export class EditListComponent implements OnInit {
|
|||
|
||||
@Input({required: true}) items: Array<string> = [];
|
||||
@Input({required: true}) label = '';
|
||||
@Input() validators: ValidatorFn[] = []
|
||||
@Input() asyncValidators: AsyncValidatorFn[] = [];
|
||||
// TODO: Make this more dynamic based on which validator failed
|
||||
@Input() errorMessage: string | null = null;
|
||||
@Output() updateItems = new EventEmitter<Array<string>>();
|
||||
|
||||
form: FormGroup = new FormGroup({items: new FormArray([])});
|
||||
|
|
@ -39,6 +43,9 @@ export class EditListComponent implements OnInit {
|
|||
|
||||
ngOnInit() {
|
||||
this.items.forEach(item => this.addItem(item));
|
||||
if (this.items.length === 0) {
|
||||
this.addItem("");
|
||||
}
|
||||
|
||||
|
||||
this.form.valueChanges.pipe(
|
||||
|
|
@ -51,7 +58,7 @@ export class EditListComponent implements OnInit {
|
|||
}
|
||||
|
||||
createItemControl(value: string = ''): FormControl {
|
||||
return new FormControl(value, []);
|
||||
return new FormControl(value, this.validators, this.asyncValidators);
|
||||
}
|
||||
|
||||
add() {
|
||||
|
|
@ -69,6 +76,7 @@ export class EditListComponent implements OnInit {
|
|||
if (this.ItemsArray.length === 1) {
|
||||
this.ItemsArray.at(0).setValue('');
|
||||
this.emit();
|
||||
this.cdRef.markForCheck();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,8 +8,7 @@
|
|||
|
||||
<app-side-nav-item cdkDrag cdkDragDisabled icon="fa-home" [title]="t('home')" link="/home/">
|
||||
<ng-container actions>
|
||||
<app-card-actionables [actions]="homeActions" labelBy="home" iconClass="fa-ellipsis-v"
|
||||
(actionHandler)="performHomeAction($event)" />
|
||||
<app-card-actionables [inputActions]="homeActions" labelBy="home" iconClass="fa-ellipsis-v" (actionHandler)="performHomeAction($event)" />
|
||||
</ng-container>
|
||||
</app-side-nav-item>
|
||||
|
||||
|
|
@ -44,8 +43,7 @@
|
|||
[imageUrl]="getLibraryImage(navStream.library!)" [title]="navStream.library!.name"
|
||||
[comparisonMethod]="'startsWith'">
|
||||
<ng-container actions>
|
||||
<app-card-actionables [actions]="actions" [labelBy]="navStream.name" iconClass="fa-ellipsis-v"
|
||||
(actionHandler)="performAction($event, navStream.library!)"></app-card-actionables>
|
||||
<app-card-actionables [entity]="navStream.library" [inputActions]="actions" [labelBy]="navStream.name" iconClass="fa-ellipsis-v"></app-card-actionables>
|
||||
</ng-container>
|
||||
</app-side-nav-item>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -155,24 +155,25 @@ export class SideNavComponent implements OnInit {
|
|||
}
|
||||
|
||||
async handleAction(action: ActionItem<Library>, library: Library) {
|
||||
const lib = library;
|
||||
switch (action.action) {
|
||||
case(Action.Scan):
|
||||
await this.actionService.scanLibrary(library);
|
||||
await this.actionService.scanLibrary(lib);
|
||||
break;
|
||||
case(Action.RefreshMetadata):
|
||||
await this.actionService.refreshLibraryMetadata(library);
|
||||
await this.actionService.refreshLibraryMetadata(lib);
|
||||
break;
|
||||
case(Action.GenerateColorScape):
|
||||
await this.actionService.refreshLibraryMetadata(library, undefined, false);
|
||||
await this.actionService.refreshLibraryMetadata(lib, undefined, false);
|
||||
break;
|
||||
case (Action.AnalyzeFiles):
|
||||
await this.actionService.analyzeFiles(library);
|
||||
await this.actionService.analyzeFiles(lib);
|
||||
break;
|
||||
case (Action.Delete):
|
||||
await this.actionService.deleteLibrary(library);
|
||||
await this.actionService.deleteLibrary(lib);
|
||||
break;
|
||||
case (Action.Edit):
|
||||
this.actionService.editLibrary(library, () => window.scrollTo(0, 0));
|
||||
this.actionService.editLibrary(lib, () => window.scrollTo(0, 0));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
|
@ -191,6 +192,7 @@ export class SideNavComponent implements OnInit {
|
|||
|
||||
performAction(action: ActionItem<Library>, library: Library) {
|
||||
if (typeof action.callback === 'function') {
|
||||
console.log('library: ', library)
|
||||
action.callback(action, library);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -130,7 +130,8 @@ export class LibrarySettingsModalComponent implements OnInit {
|
|||
|
||||
get IsMetadataDownloadEligible() {
|
||||
const libType = parseInt(this.libraryForm.get('type')?.value + '', 10) as LibraryType;
|
||||
return libType === LibraryType.Manga || libType === LibraryType.LightNovel || libType === LibraryType.ComicVine;
|
||||
return libType === LibraryType.Manga || libType === LibraryType.LightNovel
|
||||
|| libType === LibraryType.ComicVine || libType === LibraryType.Comic;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
|
@ -256,12 +257,12 @@ export class LibrarySettingsModalComponent implements OnInit {
|
|||
|
||||
// TODO: Refactor into FormArray
|
||||
for(let fileTypeGroup of allFileTypeGroup) {
|
||||
this.libraryForm.addControl(fileTypeGroup + '', new FormControl(this.library.libraryFileTypes.includes(fileTypeGroup), []));
|
||||
this.libraryForm.addControl(fileTypeGroup + '', new FormControl((this.library.libraryFileTypes || []).includes(fileTypeGroup), []));
|
||||
}
|
||||
|
||||
// TODO: Refactor into FormArray
|
||||
for(let glob of this.library.excludePatterns) {
|
||||
this.libraryForm.addControl('excludeGlob-' , new FormControl(glob, []));
|
||||
this.libraryForm.addControl('excludeGlob-', new FormControl(glob, []));
|
||||
}
|
||||
|
||||
this.excludePatterns = this.library.excludePatterns;
|
||||
|
|
|
|||
|
|
@ -72,6 +72,10 @@ export class TypeaheadComponent implements OnInit {
|
|||
* When triggered, will focus the input if the passed string matches the id
|
||||
*/
|
||||
@Input() focus: EventEmitter<string> | undefined;
|
||||
/**
|
||||
* When triggered, will unfocus the input if the passed string matches the id
|
||||
*/
|
||||
@Input() unFocus: EventEmitter<string> | undefined;
|
||||
@Output() selectedData = new EventEmitter<any[] | any>();
|
||||
@Output() newItemAdded = new EventEmitter<any[] | any>();
|
||||
// eslint-disable-next-line @angular-eslint/no-output-on-prefix
|
||||
|
|
@ -113,6 +117,13 @@ export class TypeaheadComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
if (this.unFocus) {
|
||||
this.unFocus.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((id: string) => {
|
||||
if (this.settings.id !== id) return;
|
||||
this.hasFocus = false;
|
||||
});
|
||||
}
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@
|
|||
|
||||
<div class="col-auto ms-2">
|
||||
<div class="card-actions mt-2" [ngbTooltip]="t('more-alt')">
|
||||
<app-card-actionables (actionHandler)="performAction($event)" [actions]="volumeActions" [labelBy]="series.name + ' ' + volume.minNumber" iconClass="fa-ellipsis-h" btnClass="btn-actions btn"></app-card-actionables>
|
||||
<app-card-actionables [entity]="volume" [inputActions]="volumeActions" [labelBy]="series.name + ' ' + volume.minNumber" iconClass="fa-ellipsis-h" btnClass="btn-actions btn"></app-card-actionables>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ import {UserReview} from "../_single-module/review-card/user-review";
|
|||
import {ReviewsComponent} from "../_single-module/reviews/reviews.component";
|
||||
import {ExternalRatingComponent} from "../series-detail/_components/external-rating/external-rating.component";
|
||||
import {ChapterService} from "../_services/chapter.service";
|
||||
import {User} from "../_models/user";
|
||||
|
||||
enum TabID {
|
||||
|
||||
|
|
@ -187,6 +188,7 @@ export class VolumeDetailComponent implements OnInit {
|
|||
protected readonly TabID = TabID;
|
||||
protected readonly FilterField = FilterField;
|
||||
protected readonly Breakpoint = Breakpoint;
|
||||
protected readonly encodeURIComponent = encodeURIComponent;
|
||||
|
||||
@ViewChild('scrollingBlock') scrollingBlock: ElementRef<HTMLDivElement> | undefined;
|
||||
@ViewChild('companionBar') companionBar: ElementRef<HTMLDivElement> | undefined;
|
||||
|
|
@ -211,7 +213,7 @@ export class VolumeDetailComponent implements OnInit {
|
|||
mobileSeriesImgBackground: string | undefined;
|
||||
downloadInProgress: boolean = false;
|
||||
|
||||
volumeActions: Array<ActionItem<Volume>> = this.actionFactoryService.getVolumeActions(this.handleVolumeAction.bind(this));
|
||||
volumeActions: Array<ActionItem<Volume>> = this.actionFactoryService.getVolumeActions(this.handleVolumeAction.bind(this), this.shouldRenderVolumeAction.bind(this));
|
||||
chapterActions: Array<ActionItem<Chapter>> = this.actionFactoryService.getChapterActions(this.handleChapterActionCallback.bind(this));
|
||||
|
||||
bulkActionCallback = async (action: ActionItem<Chapter>, _: any) => {
|
||||
|
|
@ -570,16 +572,6 @@ export class VolumeDetailComponent implements OnInit {
|
|||
this.location.replaceState(newUrl)
|
||||
}
|
||||
|
||||
openPerson(field: FilterField, value: number) {
|
||||
this.filterUtilityService.applyFilter(['all-series'], field, FilterComparison.Equal, `${value}`).subscribe();
|
||||
}
|
||||
|
||||
performAction(action: ActionItem<Volume>) {
|
||||
if (typeof action.callback === 'function') {
|
||||
action.callback(action, this.volume!);
|
||||
}
|
||||
}
|
||||
|
||||
async handleChapterActionCallback(action: ActionItem<Chapter>, chapter: Chapter) {
|
||||
switch (action.action) {
|
||||
case(Action.MarkAsRead):
|
||||
|
|
@ -610,6 +602,17 @@ export class VolumeDetailComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
shouldRenderVolumeAction(action: ActionItem<Volume>, entity: Volume, user: User) {
|
||||
switch (action.action) {
|
||||
case(Action.MarkAsRead):
|
||||
return entity.pagesRead < entity.pages;
|
||||
case(Action.MarkAsUnread):
|
||||
return entity.pagesRead !== 0;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
async handleVolumeAction(action: ActionItem<Volume>) {
|
||||
switch (action.action) {
|
||||
case Action.Delete:
|
||||
|
|
@ -687,6 +690,4 @@ export class VolumeDetailComponent implements OnInit {
|
|||
this.currentlyReadingChapter = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected readonly encodeURIComponent = encodeURIComponent;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,15 +62,6 @@
|
|||
"spoiler": {
|
||||
"click-to-show": "Spoiler, kliknutím zobrazíte"
|
||||
},
|
||||
"review-series-modal": {
|
||||
"title": "Upravit recenzi",
|
||||
"review-label": "Recenze",
|
||||
"close": "{{common.close}}",
|
||||
"save": "{{common.save}}",
|
||||
"delete": "{{common.delete}}",
|
||||
"min-length": "Recenze musí mít alespoň {{count}} znaků",
|
||||
"required": "{{validation.required-field}}"
|
||||
},
|
||||
"review-card-modal": {
|
||||
"close": "{{common.close}}",
|
||||
"user-review": "{{username}}'s Recenze",
|
||||
|
|
@ -81,7 +72,8 @@
|
|||
"your-review": "Toto je vaše recenze",
|
||||
"external-review": "Externí recenze",
|
||||
"local-review": "Místní recenze",
|
||||
"rating-percentage": "Hodnocení {{r}} %"
|
||||
"rating-percentage": "Hodnocení {{r}} %",
|
||||
"critic": "kritik"
|
||||
},
|
||||
"want-to-read": {
|
||||
"title": "Chci číst",
|
||||
|
|
@ -785,9 +777,7 @@
|
|||
"publication-status-tooltip": "Stav publikace",
|
||||
"release-date-title": "Vydání",
|
||||
"time-left-alt": "Zbývající čas",
|
||||
"user-reviews-local": "Místní hodnocení",
|
||||
"publication-status-title": "Publikace",
|
||||
"user-reviews-plus": "Externí hodnocení",
|
||||
"weblinks-title": "Odkazy",
|
||||
"more-alt": "Více",
|
||||
"pages-count": "{{num}} stran",
|
||||
|
|
@ -821,7 +811,8 @@
|
|||
"close": "{{common.close}}",
|
||||
"kavita-rating-title": "Vaše hodnocení",
|
||||
"entry-label": "Zobrazit podrobnosti",
|
||||
"kavita-tooltip": "Vaše hodnocení + celkové"
|
||||
"kavita-tooltip": "Vaše hodnocení + celkové",
|
||||
"critic": "{{review-card.critic}}"
|
||||
},
|
||||
"badge-expander": {
|
||||
"more-items": "a {{počet}} více"
|
||||
|
|
@ -1460,7 +1451,8 @@
|
|||
"skip-alt": "Přeskočit na hlavní obsah",
|
||||
"server-settings": "Nastavení serveru",
|
||||
"settings": "Nastavení",
|
||||
"announcements": "Oznámení"
|
||||
"announcements": "Oznámení",
|
||||
"person-aka-status": "Shoduje se s aliasem"
|
||||
},
|
||||
"promoted-icon": {
|
||||
"promoted": "{{common.promoted}}"
|
||||
|
|
@ -2009,7 +2001,10 @@
|
|||
"new-collection": "Nová sbírka",
|
||||
"match": "Shoda",
|
||||
"match-tooltip": "Přiřadit série s Kavitou+ manuálně",
|
||||
"reorder": "Přeuspořádat"
|
||||
"reorder": "Přeuspořádat",
|
||||
"rename": "Přejmenovat",
|
||||
"rename-tooltip": "Přejmenovat inteligentní filtr",
|
||||
"merge": "Sloučit"
|
||||
},
|
||||
"changelog-update-item": {
|
||||
"changed": "Změněno",
|
||||
|
|
@ -2045,7 +2040,9 @@
|
|||
"known-for-title": "Známý díky",
|
||||
"individual-role-title": "Jako {{role}}",
|
||||
"browse-person-title": "Všechny práce {{name}}",
|
||||
"anilist-url": "{{edit-person-modal.anilist-tooltip}}"
|
||||
"anilist-url": "{{edit-person-modal.anilist-tooltip}}",
|
||||
"no-info": "Žádné informace o této osobě",
|
||||
"aka-title": "Známé také jako "
|
||||
},
|
||||
"draggable-ordered-list": {
|
||||
"reorder-label": "Přeuspořádat",
|
||||
|
|
@ -2064,7 +2061,7 @@
|
|||
},
|
||||
"match-series-modal": {
|
||||
"no-results": "Nelze najít shodu.Zkuste přidat url od podporovaného poskytovatele a zkuste to znovu.",
|
||||
"query-tooltip": "Zadejte název série, url AniListu/MyAnimeListu. Url budou používat přímé vyhledávání.",
|
||||
"query-tooltip": "Zadejte název série, AniList/MyAnimeList/ComicBookRoundup url. Url budou používat přímé vyhledávání.",
|
||||
"title": "Shoda {{seriesName}}",
|
||||
"close": "{{common.close}}",
|
||||
"save": "{{common.save}}",
|
||||
|
|
@ -2131,7 +2128,8 @@
|
|||
"chapter-count": "{{common.chapter-count}}",
|
||||
"releasing": "Vychází",
|
||||
"details": "Zobrazit stránku",
|
||||
"updating-metadata-status": "Aktualizace metadat"
|
||||
"updating-metadata-status": "Aktualizace metadat",
|
||||
"issue-count": "{{common.issue-count}}"
|
||||
},
|
||||
"pdf-reader": {
|
||||
"incognito-mode": "Anonymní režim",
|
||||
|
|
@ -2179,7 +2177,11 @@
|
|||
"cover-image-tab": "{{edit-series-modal.cover-image-tab}}",
|
||||
"close": "{{common.close}}",
|
||||
"anilist-id-label": "AniList ID",
|
||||
"anilist-tooltip": "https://anilist.co/staff/{AniListId}/"
|
||||
"anilist-tooltip": "https://anilist.co/staff/{AniListId}/",
|
||||
"aliases-tab": "Aliasy",
|
||||
"aliases-label": "Upravit aliasy",
|
||||
"alias-overlap": "Tento alias již odkazuje na jinou osobu nebo je jménem této osoby, zvažte jejich sloučení.",
|
||||
"aliases-tooltip": "Pokud je série označena aliasem osoby, je tato osoba přiřazena, místo aby byla vytvořena nová osoba. Při odstranění aliasu je nutné sérii znovu prohledat, aby se změna zachytila."
|
||||
},
|
||||
"errors": {
|
||||
"delete-theme-in-use": "Motiv je aktuálně používán alespoň jedním uživatelem, nelze odstranit",
|
||||
|
|
@ -2458,7 +2460,8 @@
|
|||
"match-success": "Správně sladěné série",
|
||||
"webtoon-override": "Přepnutí do režimu Webtoon kvůli obrázkům představujícím webtoon.",
|
||||
"scrobble-gen-init": "Vytvořena úloha pro generování událostí scrobble z historie čtení, hodnocení v minulosti a jejich synchronizaci s připojenými službami.",
|
||||
"confirm-delete-multiple-volumes": "Jste si jisti, že chcete odstranit {{count}} svazků? Soubory na disku se tím nezmění."
|
||||
"confirm-delete-multiple-volumes": "Jste si jisti, že chcete odstranit {{count}} svazků? Soubory na disku se tím nezmění.",
|
||||
"series-added-want-to-read": "Série přidána ze seznamu Chci číst"
|
||||
},
|
||||
"preferences": {
|
||||
"split-right-to-left": "Rozdělit zprava doleva",
|
||||
|
|
@ -2620,7 +2623,18 @@
|
|||
"localized-name-tooltip": "Povolit zápis lokalizovaného názvu při odemknutí pole. Kavita se pokusí provést nejlepší odhad.",
|
||||
"enable-cover-image-label": "Obrázek na obálce",
|
||||
"overrides-description": "Povolit Kavitě zápis přes uzamčená pole.",
|
||||
"overrides-label": "Přepisuje"
|
||||
"overrides-label": "Přepisuje",
|
||||
"enable-chapter-release-date-label": "Datum vydání",
|
||||
"enable-chapter-publisher-label": "Vydavatel",
|
||||
"enable-chapter-publisher-tooltip": "Umožnit napsání vydavatele kapitoly/vydání",
|
||||
"enable-chapter-cover-label": "Obálka kapitoly",
|
||||
"enable-chapter-cover-tooltip": "Umožnit nastavení obálky kapitoly/vydání",
|
||||
"chapter-header": "Pole kapitoly",
|
||||
"enable-chapter-release-date-tooltip": "Povolit datum vydání kapitoly/vydání, které má být napsáno",
|
||||
"enable-chapter-title-label": "Titulek",
|
||||
"enable-chapter-title-tooltip": "Umožnit napsání názvu kapitoly/vydání",
|
||||
"enable-chapter-summary-label": "{{manage-metadata-settings.summary-label}}",
|
||||
"enable-chapter-summary-tooltip": "{{manage-metadata-settings.summary-tooltip}}"
|
||||
},
|
||||
"metadata-setting-field-pipe": {
|
||||
"people": "{{tabs.people-tab}}",
|
||||
|
|
@ -2631,7 +2645,12 @@
|
|||
"covers": "Obálky",
|
||||
"genres": "{{metadata-fields.genres-title}}",
|
||||
"summary": "{{filter-field-pipe.summary}}",
|
||||
"publication-status": "{{edit-series-modal.publication-status-title}}"
|
||||
"publication-status": "{{edit-series-modal.publication-status-title}}",
|
||||
"chapter-title": "Název (kapitola)",
|
||||
"chapter-covers": "Obálky (kapitola)",
|
||||
"chapter-release-date": "Datum vydání (kapitola)",
|
||||
"chapter-summary": "Shrnutí (kapitola)",
|
||||
"chapter-publisher": "{{person-role-pipe.publisher}} (Kapitola)"
|
||||
},
|
||||
"role-localized-pipe": {
|
||||
"download": "Stažené",
|
||||
|
|
@ -2649,5 +2668,27 @@
|
|||
"trace": "Sledovat",
|
||||
"warning": "Varování",
|
||||
"critical": "Kritické"
|
||||
},
|
||||
"review-modal": {
|
||||
"title": "Upravit recenzi",
|
||||
"min-length": "Recenze musí mít alespoň {{count}} znaků",
|
||||
"required": "{{validation.required-field}}",
|
||||
"delete": "{{common.delete}}",
|
||||
"save": "{{common.save}}",
|
||||
"review-label": "Recenze",
|
||||
"close": "{{common.close}}"
|
||||
},
|
||||
"reviews": {
|
||||
"user-reviews-local": "Místní recenze",
|
||||
"user-reviews-plus": "Externí recenze"
|
||||
},
|
||||
"merge-person-modal": {
|
||||
"title": "{{personName}}",
|
||||
"close": "{{common.close}}",
|
||||
"save": "{{common.save}}",
|
||||
"src": "Sloučit osoby",
|
||||
"merge-warning": "Pokud budete pokračovat, vybraná osoba bude odstraněna. Jméno vybrané osoby bude přidáno jako alias a všechny její role budou převedeny.",
|
||||
"alias-title": "Nové aliasy",
|
||||
"known-for-title": "Známý pro"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,12 +16,6 @@
|
|||
"filter-label": "{{common.filter}}",
|
||||
"special": "{{entity-title.special}}"
|
||||
},
|
||||
"review-series-modal": {
|
||||
"close": "{{common.close}}",
|
||||
"save": "{{common.save}}",
|
||||
"delete": "{{common.delete}}",
|
||||
"required": "{{validation.required-field}}"
|
||||
},
|
||||
"review-card-modal": {
|
||||
"close": "{{common.close}}"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -62,15 +62,6 @@
|
|||
"spoiler": {
|
||||
"click-to-show": "Spoiler, klicke zum Anzeigen"
|
||||
},
|
||||
"review-series-modal": {
|
||||
"title": "Rezension bearbeiten",
|
||||
"review-label": "Rezension",
|
||||
"close": "{{common.close}}",
|
||||
"save": "{{common.save}}",
|
||||
"delete": "{{common.delete}}",
|
||||
"min-length": "Rezension muss mindestens {{count}} Zeichen lang sein",
|
||||
"required": "{{validation.required-field}}"
|
||||
},
|
||||
"review-card-modal": {
|
||||
"close": "{{common.close}}",
|
||||
"user-review": "{{username}}'s Rezension",
|
||||
|
|
@ -762,8 +753,6 @@
|
|||
"no-pages": "{{toasts.no-pages}}",
|
||||
"no-chapters": "Zu diesem Band gibt es keine Kapitel. Kann nicht gelesen werden.",
|
||||
"cover-change": "Es kann bis zu einer Minute dauern, bis Ihr Browser das Bild aktualisiert hat. Bis dahin kann auf einigen Seiten noch das alte Bild angezeigt werden.",
|
||||
"user-reviews-local": "Lokale Bewertungen",
|
||||
"user-reviews-plus": "Externe Bewertungen",
|
||||
"writers-title": "{{metadata-fields.writers-title}}",
|
||||
"cover-artists-title": "{{metadata-fields.cover-artists-title}}",
|
||||
"characters-title": "{{metadata-fields.characters-title}}",
|
||||
|
|
|
|||
|
|
@ -55,15 +55,6 @@
|
|||
"spoiler": {
|
||||
"click-to-show": "Spoiler, κάντε κλικ για να το δείτε"
|
||||
},
|
||||
"review-series-modal": {
|
||||
"title": "Επεξεργασία Κριτικής",
|
||||
"review-label": "Κριτική",
|
||||
"close": "{{common.close}}",
|
||||
"save": "{{common.save}}",
|
||||
"delete": "{{common.delete}}",
|
||||
"min-length": "Η κριτική πρέπει να περιέχει τουλάχιστον {{count}} χαρακτήρες",
|
||||
"required": "{{validation.required-field}}"
|
||||
},
|
||||
"review-card-modal": {
|
||||
"close": "{{common.close}}",
|
||||
"user-review": "Η κριτική του {{username}}",
|
||||
|
|
@ -290,8 +281,6 @@
|
|||
"info-tab": "{{tabs.info-tab}}",
|
||||
"recommendations-tab": "{{tabs.recommendations-tab}}",
|
||||
"no-pages": "{{toasts.no-pages}}",
|
||||
"user-reviews-local": "Τοπικές Κριτικές",
|
||||
"user-reviews-plus": "Εξωτερικές Κριτικές",
|
||||
"writers-title": "{{metadata-fields.writers-title}}",
|
||||
"cover-artists-title": "{{metadata-fields.cover-artists-title}}",
|
||||
"characters-title": "{{metadata-fields.characters-title}}",
|
||||
|
|
|
|||
|
|
@ -1003,7 +1003,7 @@
|
|||
"save": "{{common.save}}",
|
||||
"no-results": "Unable to find a match. Try adding the url from a supported provider and retry.",
|
||||
"query-label": "Query",
|
||||
"query-tooltip": "Enter series name, AniList/MyAnimeList url. Urls will use a direct lookup.",
|
||||
"query-tooltip": "Enter series name, AniList/MyAnimeList/ComicBookRoundup url. Urls will use a direct lookup.",
|
||||
"dont-match-label": "Do not Match",
|
||||
"dont-match-tooltip": "Opt this series from matching and scrobbling",
|
||||
"search": "Search"
|
||||
|
|
@ -1103,12 +1103,14 @@
|
|||
},
|
||||
|
||||
"person-detail": {
|
||||
"aka-title": "Also known as ",
|
||||
"known-for-title": "Known For",
|
||||
"individual-role-title": "As a {{role}}",
|
||||
"browse-person-title": "All Works of {{name}}",
|
||||
"browse-person-by-role-title": "All Works of {{name}} as a {{role}}",
|
||||
"all-roles": "Roles",
|
||||
"anilist-url": "{{edit-person-modal.anilist-tooltip}}"
|
||||
"anilist-url": "{{edit-person-modal.anilist-tooltip}}",
|
||||
"no-info": "No information about this Person"
|
||||
},
|
||||
|
||||
"library-settings-modal": {
|
||||
|
|
@ -1857,7 +1859,8 @@
|
|||
"logout": "Logout",
|
||||
"all-filters": "Smart Filters",
|
||||
"nav-link-header": "Navigation Options",
|
||||
"close": "{{common.close}}"
|
||||
"close": "{{common.close}}",
|
||||
"person-aka-status": "Matches an alias"
|
||||
},
|
||||
|
||||
"promoted-icon": {
|
||||
|
|
@ -2246,6 +2249,7 @@
|
|||
"title": "{{personName}} Details",
|
||||
"general-tab": "{{edit-series-modal.general-tab}}",
|
||||
"cover-image-tab": "{{edit-series-modal.cover-image-tab}}",
|
||||
"aliases-tab": "Aliases",
|
||||
"loading": "{{common.loading}}",
|
||||
"close": "{{common.close}}",
|
||||
"name-label": "{{edit-series-modal.name-label}}",
|
||||
|
|
@ -2263,7 +2267,20 @@
|
|||
"cover-image-description": "{{edit-series-modal.cover-image-description}}",
|
||||
"cover-image-description-extra": "Alternatively you can download a cover from CoversDB if available.",
|
||||
"save": "{{common.save}}",
|
||||
"download-coversdb": "Download from CoversDB"
|
||||
"download-coversdb": "Download from CoversDB",
|
||||
"aliases-label": "Edit aliases",
|
||||
"alias-overlap": "This alias already points towards another person or is the name of this person, consider merging them.",
|
||||
"aliases-tooltip": "When a series is tagged with an alias of a person, the person is assigned rather than creating a new person. When deleting an alias, you'll have to rescan the series for the change to be picked up."
|
||||
},
|
||||
|
||||
"merge-person-modal": {
|
||||
"title": "{{personName}}",
|
||||
"close": "{{common.close}}",
|
||||
"save": "{{common.save}}",
|
||||
"src": "Merge Person",
|
||||
"merge-warning": "If you proceed, the selected person will be removed. The selected person's name will be added as an alias, and all their roles will be transferred.",
|
||||
"alias-title": "New aliases",
|
||||
"known-for-title": "Known for"
|
||||
},
|
||||
|
||||
"day-breakdown": {
|
||||
|
|
@ -2606,6 +2623,7 @@
|
|||
"entity-unread": "{{name}} is now unread",
|
||||
"mark-read": "Marked as Read",
|
||||
"mark-unread": "Marked as Unread",
|
||||
"series-added-want-to-read": "Series added from Want to Read list",
|
||||
"series-removed-want-to-read": "Series removed from Want to Read list",
|
||||
"series-deleted": "Series deleted",
|
||||
"delete-review": "Are you sure you want to delete your review?",
|
||||
|
|
@ -2780,7 +2798,8 @@
|
|||
"match-tooltip": "Match Series with Kavita+ manually",
|
||||
"reorder": "Reorder",
|
||||
"rename": "Rename",
|
||||
"rename-tooltip": "Rename the Smart Filter"
|
||||
"rename-tooltip": "Rename the Smart Filter",
|
||||
"merge": "Merge"
|
||||
},
|
||||
|
||||
"preferences": {
|
||||
|
|
|
|||
|
|
@ -62,15 +62,6 @@
|
|||
"spoiler": {
|
||||
"click-to-show": "Alerta de spoiler, haz clic para mostrar"
|
||||
},
|
||||
"review-series-modal": {
|
||||
"title": "Editar reseña",
|
||||
"review-label": "Reseña",
|
||||
"close": "{{common.close}}",
|
||||
"save": "{{common.save}}",
|
||||
"delete": "{{common.delete}}",
|
||||
"min-length": "La reseña debe tener al menos {{count}} caracteres",
|
||||
"required": "{{validation.required-field}}"
|
||||
},
|
||||
"review-card-modal": {
|
||||
"close": "{{common.close}}",
|
||||
"user-review": "Reseña de {{username}}",
|
||||
|
|
@ -757,8 +748,6 @@
|
|||
"no-pages": "{{toasts.no-pages}}",
|
||||
"no-chapters": "No existen capítulos en este volumen. No se puede leer.",
|
||||
"cover-change": "Puede tomar hasta un minuto para que tu navegador actualice la imagen. Espera hasta entonces. La imagen antigua se puede mostrar en algunas páginas.",
|
||||
"user-reviews-local": "Reseñas locales",
|
||||
"user-reviews-plus": "Reseñas externas",
|
||||
"writers-title": "{{metadata-fields.writers-title}}",
|
||||
"cover-artists-title": "{{metadata-fields.cover-artists-title}}",
|
||||
"characters-title": "{{metadata-fields.characters-title}}",
|
||||
|
|
|
|||
|
|
@ -56,15 +56,6 @@
|
|||
"spoiler": {
|
||||
"click-to-show": "Potentsiaalselt mittesoovitav eelinfo sisu kohta - kliki seda, et näidata"
|
||||
},
|
||||
"review-series-modal": {
|
||||
"title": "Muuda ülevaade",
|
||||
"review-label": "Ülevaade",
|
||||
"close": "{{common.close}}",
|
||||
"save": "{{common.save}}",
|
||||
"delete": "{{common.delete}}",
|
||||
"min-length": "Ülevaade peab olema vähemalt {{count}} märki pikk",
|
||||
"required": "{{validation.required-field}}"
|
||||
},
|
||||
"review-card-modal": {
|
||||
"close": "{{common.close}}",
|
||||
"user-review": "{{username}} ülevaade",
|
||||
|
|
|
|||
|
|
@ -128,15 +128,6 @@
|
|||
"spoiler": {
|
||||
"click-to-show": "Spoileri, napsauta näyttääksesi"
|
||||
},
|
||||
"review-series-modal": {
|
||||
"title": "Muokkaa arvostelua",
|
||||
"review-label": "Arvostelu",
|
||||
"close": "{{common.close}}",
|
||||
"save": "{{common.save}}",
|
||||
"delete": "{{common.delete}}",
|
||||
"min-length": "Arvostelussa on oltava vähintään {{count}} merkkiä",
|
||||
"required": "{{validation.required-field}}"
|
||||
},
|
||||
"review-card-modal": {
|
||||
"close": "{{common.close}}",
|
||||
"external-mod": "(external)",
|
||||
|
|
|
|||
|
|
@ -62,15 +62,6 @@
|
|||
"spoiler": {
|
||||
"click-to-show": "Divulgâcheur, cliquez afin d'afficher"
|
||||
},
|
||||
"review-series-modal": {
|
||||
"title": "Éditer la critique",
|
||||
"review-label": "Critique",
|
||||
"close": "{{common.close}}",
|
||||
"save": "{{common.save}}",
|
||||
"delete": "{{common.delete}}",
|
||||
"min-length": "La critique doit comporter au moins {{count}} caractères",
|
||||
"required": "{{validation.required-field}}"
|
||||
},
|
||||
"review-card-modal": {
|
||||
"close": "{{common.close}}",
|
||||
"user-review": "Critique de {{username}}",
|
||||
|
|
@ -762,8 +753,6 @@
|
|||
"no-pages": "{{toasts.no-pages}}",
|
||||
"no-chapters": "Il n'y a aucun chapitre dans ce volume. Il ne peut être lu.",
|
||||
"cover-change": "L'actualisation de l'image par votre navigateur peut prendre jusqu'à une minute. En attendant, l'ancienne image peut être affichée sur certaines pages.",
|
||||
"user-reviews-local": "Commentaires locaux",
|
||||
"user-reviews-plus": "Commentaires externes",
|
||||
"writers-title": "{{metadata-fields.writers-title}}",
|
||||
"cover-artists-title": "{{metadata-fields.cover-artists-title}}",
|
||||
"characters-title": "{{metadata-fields.characters-title}}",
|
||||
|
|
|
|||
|
|
@ -62,15 +62,6 @@
|
|||
"spoiler": {
|
||||
"click-to-show": "Spoiler, cliceáil chun a thaispeáint"
|
||||
},
|
||||
"review-series-modal": {
|
||||
"title": "Cuir Athbhreithniú in Eagar",
|
||||
"review-label": "Athbhreithniú",
|
||||
"close": "{{common.close}}",
|
||||
"save": "{{common.save}}",
|
||||
"delete": "{{common.delete}}",
|
||||
"min-length": "Ní mór an t-athbhreithniú a bheith ar a laghad {{count}} carachtair",
|
||||
"required": "{{validation.required-field}}"
|
||||
},
|
||||
"review-card-modal": {
|
||||
"close": "{{common.close}}",
|
||||
"user-review": "Léirmheas {{username}}",
|
||||
|
|
@ -81,7 +72,8 @@
|
|||
"your-review": "Is é seo d'athbhreithniú",
|
||||
"external-review": "Athbhreithniú Seachtrach",
|
||||
"local-review": "Athbhreithniú Áitiúil",
|
||||
"rating-percentage": "Rátáil {{r}}%"
|
||||
"rating-percentage": "Rátáil {{r}}%",
|
||||
"critic": "criticeoir"
|
||||
},
|
||||
"want-to-read": {
|
||||
"title": "Ag iarraidh léamh",
|
||||
|
|
@ -762,8 +754,6 @@
|
|||
"no-pages": "{{toasts.no-pages}}",
|
||||
"no-chapters": "Níl aon chaibidlí san imleabhar seo. Ní féidir léamh.",
|
||||
"cover-change": "Is féidir leis suas le nóiméad a thógáil le do bhrabhsálaí an íomhá a athnuachan. Go dtí sin, is féidir an tseaníomhá a thaispeáint ar roinnt leathanach.",
|
||||
"user-reviews-local": "Léirmheasanna Áitiúla",
|
||||
"user-reviews-plus": "Léirmheasanna Seachtracha",
|
||||
"writers-title": "{{metadata-fields.writers-title}}",
|
||||
"cover-artists-title": "{{metadata-fields.cover-artists-title}}",
|
||||
"characters-title": "{{metadata-fields.characters-title}}",
|
||||
|
|
@ -825,7 +815,8 @@
|
|||
"entry-label": "Féach Sonraí",
|
||||
"kavita-tooltip": "Do Rátáil + Tríd is tríd",
|
||||
"kavita-rating-title": "Do Rátáil",
|
||||
"close": "{{common.close}}"
|
||||
"close": "{{common.close}}",
|
||||
"critic": "{{review-card.critic}}"
|
||||
},
|
||||
"badge-expander": {
|
||||
"more-items": "agus {{count}} níos mó"
|
||||
|
|
@ -1522,7 +1513,8 @@
|
|||
"logout": "Logáil amach",
|
||||
"all-filters": "Scagairí Cliste",
|
||||
"nav-link-header": "Roghanna Nascleanúna",
|
||||
"close": "{{common.close}}"
|
||||
"close": "{{common.close}}",
|
||||
"person-aka-status": "Meaitseálann leasainm"
|
||||
},
|
||||
"promoted-icon": {
|
||||
"promoted": "{{common.promoted}}"
|
||||
|
|
@ -2260,7 +2252,9 @@
|
|||
"bulk-delete-libraries": "An bhfuil tú cinnte gur mhaith leat {{count}} leabharlann a scriosadh?",
|
||||
"match-success": "Sraith a mheaitseáil i gceart",
|
||||
"webtoon-override": "Ag aistriú go mód Webtoon mar gheall ar íomhánna a léiríonn webtoon.",
|
||||
"scrobble-gen-init": "Cheangail post chun teagmhais scrobble a ghiniúint ó stair léitheoireachta agus rátálacha san am a chuaigh thart, agus iad á sioncronú le seirbhísí ceangailte."
|
||||
"scrobble-gen-init": "Cheangail post chun teagmhais scrobble a ghiniúint ó stair léitheoireachta agus rátálacha san am a chuaigh thart, agus iad á sioncronú le seirbhísí ceangailte.",
|
||||
"confirm-delete-multiple-volumes": "An bhfuil tú cinnte gur mian leat {{count}} imleabhar a scriosadh? Ní athróidh sé comhaid ar an diosca.",
|
||||
"series-added-want-to-read": "Sraith curtha leis ón liosta Ar Mhaith Leat Léamh"
|
||||
},
|
||||
"read-time-pipe": {
|
||||
"less-than-hour": "<1 Uair",
|
||||
|
|
@ -2333,7 +2327,10 @@
|
|||
"copy-settings": "Cóipeáil Socruithe Ó",
|
||||
"match": "Meaitseáil",
|
||||
"match-tooltip": "Sraith Meaitseála le Kavita+ de láimh",
|
||||
"reorder": "Athordú"
|
||||
"reorder": "Athordú",
|
||||
"rename": "Athainmnigh",
|
||||
"rename-tooltip": "Athainmnigh an Scagaire Cliste",
|
||||
"merge": "Cumaisc"
|
||||
},
|
||||
"preferences": {
|
||||
"left-to-right": "Clé go Deas",
|
||||
|
|
@ -2472,7 +2469,9 @@
|
|||
"browse-person-by-role-title": "Gach Oibre de {{name}} mar {{role}}",
|
||||
"individual-role-title": "Mar {{role}}",
|
||||
"all-roles": "Róil",
|
||||
"anilist-url": "{{edit-person-modal.anilist-tooltip}}"
|
||||
"anilist-url": "{{edit-person-modal.anilist-tooltip}}",
|
||||
"aka-title": "Ar a dtugtar freisin ",
|
||||
"no-info": "Gan aon eolas faoin Duine seo"
|
||||
},
|
||||
"edit-person-modal": {
|
||||
"general-tab": "{{edit-series-modal.general-tab}}",
|
||||
|
|
@ -2495,7 +2494,11 @@
|
|||
"anilist-tooltip": "https://anilist.co/staff/{AniListId}/",
|
||||
"cover-image-description-extra": "Nó is féidir leat clúdach a íoslódáil ó CoversDB má tá sé ar fáil.",
|
||||
"hardcover-tooltip": "https://hardcover.app/authors/{HardcoverId}",
|
||||
"download-coversdb": "Íoslódáil ó CoverDB"
|
||||
"download-coversdb": "Íoslódáil ó CoverDB",
|
||||
"aliases-label": "Cuir leasainmneacha in eagar",
|
||||
"aliases-tab": "Leasainmneacha",
|
||||
"aliases-tooltip": "Nuair a chuirtear leasainm duine ar shraith, sanntar an duine sin seachas duine nua a chruthú. Agus leasainm á scriosadh, beidh ort an tsraith a athscanadh le go dtabharfar faoi deara an t-athrú.",
|
||||
"alias-overlap": "Tá an leasainm seo ag tagairt do dhuine eile cheana féin nó is é ainm an duine seo é, smaoinigh ar iad a chumasc."
|
||||
},
|
||||
"new-version-modal": {
|
||||
"description": "Tá leagan nua de Kavita ar fáil. Athnuaigh le nuashonrú.",
|
||||
|
|
@ -2532,7 +2535,7 @@
|
|||
"title": "Meaitseáil {{ seriesName}}",
|
||||
"description": "Roghnaigh meaitseáil chun meiteashonraí Kavita+ a athshreangú agus imeachtaí scrobble a athghiniúint. Is féidir Don't Match a úsáid chun Kavita a shrianadh ó mheiteashonraí a mheaitseáil agus scrobadh a dhéanamh.",
|
||||
"close": "{{common.close}}",
|
||||
"query-tooltip": "Cuir isteach ainm na sraithe, url AniList/MyAnimeList. Bainfidh URLanna úsáid as cuardach díreach.",
|
||||
"query-tooltip": "Cuir isteach ainm na sraithe, url AniList/MyAnimeList/ComicBookRoundup. Úsáidfear cuardach díreach sna URLanna.",
|
||||
"dont-match-label": "Ná Meaitseáil",
|
||||
"dont-match-tooltip": "Rogha an tsraith seo ó mheaitseáil agus scrobbling",
|
||||
"search": "Cuardach"
|
||||
|
|
@ -2569,7 +2572,8 @@
|
|||
"volume-count": "{{server-stats.volume-count}}",
|
||||
"chapter-count": "{{common.chapter-count}}",
|
||||
"releasing": "Ag scaoileadh",
|
||||
"updating-metadata-status": "Meiteashonraí á nuashonrú"
|
||||
"updating-metadata-status": "Meiteashonraí á nuashonrú",
|
||||
"issue-count": "{{common.issue-count}}"
|
||||
},
|
||||
"manage-user-tokens": {
|
||||
"description": "Seans go mbeidh gá le hathnuachan a dhéanamh ó am go chéile ar úsáideoirí a bhfuil comharthaí scrobarnaí acu. Seolfaidh Kavita ríomhphost chucu go huathoibríoch má tá ríomhphost socraithe agus má tá ríomhphost bailí acu.",
|
||||
|
|
@ -2619,7 +2623,18 @@
|
|||
"overrides-description": "Lig do Kavita scríobh thar réimsí faoi ghlas.",
|
||||
"overrides-label": "Sáraíonn",
|
||||
"localized-name-tooltip": "Ceadaigh Ainm Logánaithe a scríobh nuair a dhíghlasáiltear an réimse. Déanfaidh Kavita iarracht an buille faoi thuairim is fearr a dhéanamh.",
|
||||
"enable-cover-image-tooltip": "Lig do Kavita íomhá clúdaigh na Sraithe a scríobh"
|
||||
"enable-cover-image-tooltip": "Lig do Kavita íomhá clúdaigh na Sraithe a scríobh",
|
||||
"enable-chapter-publisher-tooltip": "Ceadaigh d’Fhoilsitheoir na Caibidle/na hEagráine a bheith scríofa",
|
||||
"chapter-header": "Réimsí Caibidle",
|
||||
"enable-chapter-title-label": "Teideal",
|
||||
"enable-chapter-title-tooltip": "Ceadaigh Teideal na Caibidle/na hEagráin a scríobh",
|
||||
"enable-chapter-summary-label": "{{manage-metadata-settings.summary-label}}",
|
||||
"enable-chapter-summary-tooltip": "{{manage-metadata-settings.summary-tooltip}}",
|
||||
"enable-chapter-release-date-label": "Dáta Eisiúna",
|
||||
"enable-chapter-release-date-tooltip": "Ceadaigh Dáta Eisiúna na Caibidle/na hEagráine a scríobh",
|
||||
"enable-chapter-publisher-label": "Foilsitheoir",
|
||||
"enable-chapter-cover-label": "Clúdach Caibidle",
|
||||
"enable-chapter-cover-tooltip": "Ceadaigh Clúdach na Caibidle/na hEagráin a shocrú"
|
||||
},
|
||||
"metadata-setting-field-pipe": {
|
||||
"people": "{{tabs.people-tab}}",
|
||||
|
|
@ -2630,7 +2645,12 @@
|
|||
"covers": "Clúdaíonn",
|
||||
"publication-status": "{{edit-series-modal.publication-status-title}}",
|
||||
"summary": "{{filter-field-pipe.summary}}",
|
||||
"age-rating": "{{metadata-fields.age-rating-title}}"
|
||||
"age-rating": "{{metadata-fields.age-rating-title}}",
|
||||
"chapter-covers": "Clúdaigh (Caibidil)",
|
||||
"chapter-release-date": "Dáta Eisiúna (Caibidil)",
|
||||
"chapter-title": "Teideal (Caibidil)",
|
||||
"chapter-summary": "Achoimre (Caibidil)",
|
||||
"chapter-publisher": "{{person-role-pipe.publisher}} (Caibidil)"
|
||||
},
|
||||
"role-localized-pipe": {
|
||||
"admin": "Riarachán",
|
||||
|
|
@ -2648,5 +2668,27 @@
|
|||
"critical": "Criticiúil",
|
||||
"information": "Eolas",
|
||||
"debug": "Dífhabhtaithe"
|
||||
},
|
||||
"review-modal": {
|
||||
"required": "{{validation.required-field}}",
|
||||
"title": "Cuir Athbhreithniú in Eagar",
|
||||
"save": "{{common.save}}",
|
||||
"min-length": "Ní mór don léirmheas a bheith {{count}} carachtar ar a laghad",
|
||||
"delete": "{{common.delete}}",
|
||||
"review-label": "Athbhreithniú",
|
||||
"close": "{{common.close}}"
|
||||
},
|
||||
"reviews": {
|
||||
"user-reviews-local": "Léirmheasanna Áitiúla",
|
||||
"user-reviews-plus": "Léirmheasanna Seachtracha"
|
||||
},
|
||||
"merge-person-modal": {
|
||||
"close": "{{common.close}}",
|
||||
"alias-title": "Leasainmneacha nua",
|
||||
"merge-warning": "Má leanann tú ar aghaidh, bainfear an duine roghnaithe. Cuirfear ainm an duine roghnaithe leis mar leasainm, agus aistreofar a róil go léir.",
|
||||
"known-for-title": "Ar a dtugtar",
|
||||
"src": "Cumaisc Duine",
|
||||
"title": "{{personName}}",
|
||||
"save": "{{common.save}}"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,12 +17,6 @@
|
|||
"filter-label": "{{common.filter}}",
|
||||
"special": "{{entity-title.special}}"
|
||||
},
|
||||
"review-series-modal": {
|
||||
"close": "{{common.close}}",
|
||||
"save": "{{common.save}}",
|
||||
"delete": "{{common.delete}}",
|
||||
"required": "{{validation.required-field}}"
|
||||
},
|
||||
"review-card-modal": {
|
||||
"close": "{{common.close}}"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -56,15 +56,6 @@
|
|||
"spoiler": {
|
||||
"click-to-show": "Spoiler, kattints a megnézéshez"
|
||||
},
|
||||
"review-series-modal": {
|
||||
"title": "Áttekintés szerkesztése",
|
||||
"review-label": "Áttekintés",
|
||||
"close": "{{common.close}}",
|
||||
"save": "{{common.save}}",
|
||||
"delete": "{{common.delete}}",
|
||||
"min-length": "Az áttekintés legalább {{count}} karakter kell legyen",
|
||||
"required": "{{validation.required-field}}"
|
||||
},
|
||||
"review-card-modal": {
|
||||
"close": "{{common.close}}",
|
||||
"user-review": "{{username}} áttekintése",
|
||||
|
|
|
|||
|
|
@ -54,15 +54,6 @@
|
|||
"spoiler": {
|
||||
"click-to-show": "Spoiler, klik untuk tampilkan"
|
||||
},
|
||||
"review-series-modal": {
|
||||
"title": "Ubah Ulasan",
|
||||
"review-label": "Ulasan",
|
||||
"close": "{{common.close}}",
|
||||
"save": "{{common.save}}",
|
||||
"delete": "{{common.delete}}",
|
||||
"min-length": "Ulasan harus setidaknya terdiri dari {{count}} karakter",
|
||||
"required": "{{validation.required-field}}"
|
||||
},
|
||||
"review-card-modal": {
|
||||
"close": "{{common.close}}"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -62,15 +62,6 @@
|
|||
"spoiler": {
|
||||
"click-to-show": "Spoiler, clicca per mostrare"
|
||||
},
|
||||
"review-series-modal": {
|
||||
"title": "Modifica recensione",
|
||||
"review-label": "Recensione",
|
||||
"close": "{{common.close}}",
|
||||
"save": "{{common.save}}",
|
||||
"delete": "{{common.delete}}",
|
||||
"min-length": "La recensione deve avere almeno {{count}} caratteri",
|
||||
"required": "{{validation.required-field}}"
|
||||
},
|
||||
"review-card-modal": {
|
||||
"close": "{{common.close}}",
|
||||
"user-review": "Recensione di {{username}}",
|
||||
|
|
@ -780,10 +771,8 @@
|
|||
"reading-lists-title": "{{side-nav.reading-lists}}",
|
||||
"time-to-read-alt": "{{sort-field-pipe.time-to-read}}",
|
||||
"scrobbling-tooltip": "{{settings.scrobbling}}",
|
||||
"user-reviews-plus": "Recensioni Esterne",
|
||||
"words-count": "{{num}} Parole",
|
||||
"more-alt": "Ancora",
|
||||
"user-reviews-local": "Recensioni Locali",
|
||||
"pages-count": "{{num}} Pagine",
|
||||
"weblinks-title": "Link",
|
||||
"time-left-alt": "Tempo Rimanente",
|
||||
|
|
|
|||
|
|
@ -61,15 +61,6 @@
|
|||
"spoiler": {
|
||||
"click-to-show": "クリックするとネタバレが表示されます"
|
||||
},
|
||||
"review-series-modal": {
|
||||
"title": "レビューを編集する",
|
||||
"review-label": "レビュー",
|
||||
"close": "{{common.close}}",
|
||||
"save": "{{common.save}}",
|
||||
"delete": "{{common.delete}}",
|
||||
"min-length": "レビューは最低 {{count}}文字以上である必要があります。",
|
||||
"required": "{{validation.required-field}}"
|
||||
},
|
||||
"review-card-modal": {
|
||||
"close": "{{common.close}}",
|
||||
"user-review": "{{username}}さんのレビュー",
|
||||
|
|
@ -726,8 +717,6 @@
|
|||
"no-pages": "{{toasts.no-pages}}",
|
||||
"no-chapters": "この巻にはチャプターが存在しません。読むことはできません。",
|
||||
"cover-change": "ブラウザが画像をリフレッシュするまで最大1分かかる場合があります。その間、一部のページでは古い画像が表示される可能性があります。",
|
||||
"user-reviews-local": "ローカルレビュー",
|
||||
"user-reviews-plus": "外部レビュー",
|
||||
"writers-title": "{{metadata-fields.writers-title}}",
|
||||
"cover-artists-title": "{{metadata-fields.cover-artists-title}}",
|
||||
"characters-title": "{{metadata-fields.characters-title}}",
|
||||
|
|
|
|||
|
|
@ -62,15 +62,6 @@
|
|||
"spoiler": {
|
||||
"click-to-show": "스포일러, 표시하려면 클릭"
|
||||
},
|
||||
"review-series-modal": {
|
||||
"title": "리뷰 수정",
|
||||
"review-label": "평가",
|
||||
"close": "{{common.close}}",
|
||||
"save": "{{common.save}}",
|
||||
"delete": "{{common.delete}}",
|
||||
"min-length": "리뷰는 적어도 {{count}}자여야 합니다",
|
||||
"required": "{{validation.required-field}}"
|
||||
},
|
||||
"review-card-modal": {
|
||||
"close": "{{common.close}}",
|
||||
"user-review": "{{username}}님의 리뷰",
|
||||
|
|
@ -81,7 +72,8 @@
|
|||
"your-review": "당신의 리뷰",
|
||||
"external-review": "외부 리뷰",
|
||||
"local-review": "로컬 리뷰",
|
||||
"rating-percentage": "평점 {{r}}%"
|
||||
"rating-percentage": "평점 {{r}}%",
|
||||
"critic": "평점"
|
||||
},
|
||||
"want-to-read": {
|
||||
"title": "읽고 싶어요",
|
||||
|
|
@ -517,9 +509,9 @@
|
|||
},
|
||||
"library-type-pipe": {
|
||||
"book": "책",
|
||||
"comic": "만화(Comic)",
|
||||
"comic": "만화 (Legacy)",
|
||||
"manga": "만화(Manga)",
|
||||
"comicVine": "Comic Vine",
|
||||
"comicVine": "코믹(Comic)",
|
||||
"image": "이미지",
|
||||
"lightNovel": "라이트 노벨"
|
||||
},
|
||||
|
|
@ -762,8 +754,6 @@
|
|||
"no-pages": "{{toasts.no-pages}}",
|
||||
"no-chapters": "이 볼륨에는 챕터가 없습니다. 읽을 수 없습니다.",
|
||||
"cover-change": "브라우저에서 이미지를 새로 고치는 데 최대 1분이 걸릴 수 있습니다. 그때까지는 일부 페이지에 이전 이미지가 표시될 수 있습니다.",
|
||||
"user-reviews-local": "로컬 리뷰",
|
||||
"user-reviews-plus": "외부 리뷰",
|
||||
"writers-title": "{{metadata-fields.writers-title}}",
|
||||
"cover-artists-title": "{{metadata-fields.cover-artists-title}}",
|
||||
"characters-title": "{{metadata-fields.characters-title}}",
|
||||
|
|
@ -789,7 +779,7 @@
|
|||
"more-alt": "더 보기",
|
||||
"time-left-alt": "남은 시간",
|
||||
"time-to-read-alt": "{{sort-field-pipe.time-to-read}}",
|
||||
"scrobbling-tooltip": "{{settings.scrobbling}}",
|
||||
"scrobbling-tooltip": "{{settings.scrobbling}}: {{value}}",
|
||||
"publication-status-title": "출판",
|
||||
"publication-status-tooltip": "출판현황",
|
||||
"on": "{{reader-settings.on}}",
|
||||
|
|
@ -825,7 +815,8 @@
|
|||
"entry-label": "세부정보 보기",
|
||||
"kavita-tooltip": "귀하의 평가 + 전체",
|
||||
"kavita-rating-title": "귀하의 평가",
|
||||
"close": "{{common.close}}"
|
||||
"close": "{{common.close}}",
|
||||
"critic": "{{review-card.critic}}"
|
||||
},
|
||||
"badge-expander": {
|
||||
"more-items": "와 {{count}} 더"
|
||||
|
|
@ -861,7 +852,8 @@
|
|||
"more": "더 보기",
|
||||
"customize": "{{settings.customize}}",
|
||||
"browse-authors": "작가 찾아보기",
|
||||
"edit": "{{common.edit}}"
|
||||
"edit": "{{common.edit}}",
|
||||
"cancel-edit": "재정렬 닫기"
|
||||
},
|
||||
"library-settings-modal": {
|
||||
"close": "{{common.close}}",
|
||||
|
|
@ -878,7 +870,7 @@
|
|||
"type-label": "유형",
|
||||
"type-tooltip": "라이브러리 유형에 따라 파일 이름이 구문 분석되는 방식과 UI에 장(만화)과 이슈(만화)가 표시되는지 여부가 결정됩니다. 라이브러리 유형 간의 차이점에 대한 자세한 내용은 Wiki를 확인하세요.",
|
||||
"kavitaplus-eligible-label": "Kavita+ 적격",
|
||||
"kavitaplus-eligible-tooltip": "Kavita+가 정보를 가져오거나 스크로블링을 지원합니까",
|
||||
"kavitaplus-eligible-tooltip": "Kavita+ 메타데이터 기능 또는 자동 추천 지원",
|
||||
"folder-description": "라이브러리에 폴더 추가",
|
||||
"browse": "미디어 폴더 찾아보기",
|
||||
"help-us-part-1": "팔로우하여 저희를 도와주세요 ",
|
||||
|
|
@ -1086,7 +1078,7 @@
|
|||
"reset": "{{common.reset}}",
|
||||
"test": "테스트",
|
||||
"host-name-label": "호스트 이름",
|
||||
"host-name-tooltip": "도메인 이름(역방향 프록시의). 이메일 기능에 필요합니다. 역방향 프록시가 없으면 임의의 URL을 사용하십시오.",
|
||||
"host-name-tooltip": "이메일 기능에 필요한 역방향 프록시의 도메인 이름입니다. 역방향 프록시를 사용하지 않는 경우, http://externalip:port/ 형식 등 어떠한 URL도 사용할 수 있습니다",
|
||||
"host-name-validation": "호스트 이름은 http(s)로 시작하고 /로 끝나지 않아야 합니다",
|
||||
"sender-address-label": "송신자 주소",
|
||||
"sender-address-tooltip": "이는 수신자가 이메일을 받을 때 볼 수 있는 이메일 주소입니다. 일반적으로 계정과 연결된 이메일 주소입니다.",
|
||||
|
|
@ -1596,7 +1588,7 @@
|
|||
"dry-run-step": "모의 실행",
|
||||
"final-import-step": "마지막 단계",
|
||||
"comicvine-parsing-label": "Comic Vine 시리즈 매칭 사용",
|
||||
"cbl-repo": "커뮤니티에서 많은 독서 목록을 찾을 수 있습니다. <a href='https://github.com/DieselTech/CBL-ReadingLists' target='_blank' rel='noopener noreferrer'>보고</a>.",
|
||||
"cbl-repo": "커뮤니티에서 많은 독서 목록을 찾을 수 있습니다. <a href='https://github.com/DieselTech/CBL-ReadingLists' target='_blank' rel='noopener noreferrer'>리포지토리</a>.",
|
||||
"help-label": "{{common.help}}"
|
||||
},
|
||||
"pdf-reader": {
|
||||
|
|
@ -2015,7 +2007,8 @@
|
|||
"sidenav": "사이드 내비게이션",
|
||||
"external-sources": "외부 소스",
|
||||
"smart-filters": "스마트 필터",
|
||||
"help": "{{common.help}}"
|
||||
"help": "{{common.help}}",
|
||||
"description": "순서 바꾸기, 표시 여부 전환, 스마트 필터 및 외부 소스를 홈페이지 또는 사이드 탐색 메뉴에 바인딩하여 Kavita의 다양한 기능을 사용자 지정할 수 있습니다."
|
||||
},
|
||||
"customize-dashboard-streams": {
|
||||
"no-data": "대시보드에 모든 스마트 필터가 추가되었거나 아직 생성되지 않았습니다.",
|
||||
|
|
@ -2258,7 +2251,8 @@
|
|||
"bulk-delete-libraries": "{{count}}개의 라이브러리를 삭제하시겠습니까?",
|
||||
"webtoon-override": "웹툰을 나타내는 이미지가 있어 웹툰 모드로 전환합니다.",
|
||||
"match-success": "시리즈가 올바르게 일치함",
|
||||
"confirm-delete-multiple-volumes": "{{count}}개의 볼륨을 삭제하시겠습니까? 디스크의 파일은 수정되지 않습니다."
|
||||
"confirm-delete-multiple-volumes": "{{count}}개의 볼륨을 삭제하시겠습니까? 디스크의 파일은 수정되지 않습니다.",
|
||||
"scrobble-gen-init": "과거 열람 이력 및 평가를 추적하는 이벤트를 생성하여 연결된 서비스와 동기화하는 기능을 작업 예정 목록에 추가하였습니다."
|
||||
},
|
||||
"read-time-pipe": {
|
||||
"less-than-hour": "<1시간",
|
||||
|
|
@ -2330,7 +2324,10 @@
|
|||
"title": "작업",
|
||||
"copy-settings": "설정 복사하기",
|
||||
"match": "일치",
|
||||
"match-tooltip": "Kavita+에서 시리즈를 수동으로 일치시키기"
|
||||
"match-tooltip": "Kavita+에서 시리즈를 수동으로 일치시키기",
|
||||
"reorder": "정렬 변경",
|
||||
"rename": "이름 변경",
|
||||
"rename-tooltip": "스마트 필터 이름 변경"
|
||||
},
|
||||
"preferences": {
|
||||
"left-to-right": "왼쪽에서 오른쪽",
|
||||
|
|
@ -2587,7 +2584,18 @@
|
|||
"enable-genres-tooltip": "시리즈 장르를 작성할 수 있도록 허용합니다.",
|
||||
"whitelist-label": "화이트리스트 태그",
|
||||
"age-rating-mapping-title": "연령 등급 매핑",
|
||||
"genre": "장르"
|
||||
"genre": "장르",
|
||||
"enable-chapter-title-label": "제목",
|
||||
"chapter-header": "챕터 필드",
|
||||
"enable-chapter-title-tooltip": "Chapter/Issue 제목 작성 허용",
|
||||
"enable-chapter-summary-label": "{{manage-metadata-settings.summary-label}}",
|
||||
"enable-chapter-summary-tooltip": "{{manage-metadata-settings.summary-tooltip}}",
|
||||
"enable-chapter-release-date-label": "발매일",
|
||||
"enable-chapter-release-date-tooltip": "Chapter/Issue 발매일 작성 허용",
|
||||
"enable-chapter-publisher-label": "출판사",
|
||||
"enable-chapter-publisher-tooltip": "Chapter/Issue 출판사 작성 허용",
|
||||
"enable-chapter-cover-label": "챕터 표지",
|
||||
"enable-chapter-cover-tooltip": "Chapter/Issue 커버 설정 허용"
|
||||
},
|
||||
"match-series-modal": {
|
||||
"search": "검색",
|
||||
|
|
@ -2606,7 +2614,8 @@
|
|||
"details": "페이지 보기",
|
||||
"updating-metadata-status": "메타데이터 업데이트",
|
||||
"releasing": "출시",
|
||||
"chapter-count": "{{common.chapter-count}}"
|
||||
"chapter-count": "{{common.chapter-count}}",
|
||||
"issue-count": "{{common.issue-count}}"
|
||||
},
|
||||
"email-history": {
|
||||
"sent-header": "성공",
|
||||
|
|
@ -2627,7 +2636,12 @@
|
|||
"people": "{{tabs.people-tab}}",
|
||||
"summary": "{{filter-field-pipe.summary}}",
|
||||
"publication-status": "{{edit-series-modal.publication-status-title}}",
|
||||
"start-date": "{{manage-metadata-settings.enable-start-date-label}}"
|
||||
"start-date": "{{manage-metadata-settings.enable-start-date-label}}",
|
||||
"chapter-publisher": "{{person-role-pipe.publisher}} (Chapter)",
|
||||
"chapter-covers": "커버 (Chapter)",
|
||||
"chapter-title": "제목 (챕터)",
|
||||
"chapter-release-date": "발매일 (Chapter)",
|
||||
"chapter-summary": "요약 (Chapter)"
|
||||
},
|
||||
"role-localized-pipe": {
|
||||
"admin": "관리자",
|
||||
|
|
@ -2645,5 +2659,18 @@
|
|||
"trace": "추적",
|
||||
"warning": "경고",
|
||||
"critical": "치명적"
|
||||
},
|
||||
"reviews": {
|
||||
"user-reviews-local": "로컬 리뷰",
|
||||
"user-reviews-plus": "외부 리뷰"
|
||||
},
|
||||
"review-modal": {
|
||||
"title": "리뷰 편집",
|
||||
"review-label": "리뷰",
|
||||
"close": "{{common.close}}",
|
||||
"save": "{{common.save}}",
|
||||
"delete": "{{common.delete}}",
|
||||
"min-length": "리뷰는 최소 {{count}}자 이상이어야 합니다",
|
||||
"required": "{{validation.required-field}}"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,12 +27,6 @@
|
|||
"filter-label": "{{common.filter}}",
|
||||
"special": "{{entity-title.special}}"
|
||||
},
|
||||
"review-series-modal": {
|
||||
"close": "{{common.close}}",
|
||||
"save": "{{common.save}}",
|
||||
"delete": "{{common.delete}}",
|
||||
"required": "{{validation.required-field}}"
|
||||
},
|
||||
"review-card-modal": {
|
||||
"close": "{{common.close}}"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -16,12 +16,6 @@
|
|||
"filter-label": "{{common.filter}}",
|
||||
"special": "{{entity-title.special}}"
|
||||
},
|
||||
"review-series-modal": {
|
||||
"close": "{{common.close}}",
|
||||
"save": "{{common.save}}",
|
||||
"delete": "{{common.delete}}",
|
||||
"required": "{{validation.required-field}}"
|
||||
},
|
||||
"review-card-modal": {
|
||||
"close": "{{common.close}}"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -62,15 +62,6 @@
|
|||
"spoiler": {
|
||||
"click-to-show": "Spoiler, klik om te tonen"
|
||||
},
|
||||
"review-series-modal": {
|
||||
"title": "Beoordeling bewerken",
|
||||
"review-label": "Beoordeling",
|
||||
"close": "{{common.close}}",
|
||||
"save": "{{common.save}}",
|
||||
"delete": "{{common.delete}}",
|
||||
"min-length": "Review moet minimum {{count}} characters hebben",
|
||||
"required": "{{validation.required-field}}"
|
||||
},
|
||||
"review-card-modal": {
|
||||
"close": "{{common.close}}",
|
||||
"user-review": "{{username}}s Recensie",
|
||||
|
|
|
|||
|
|
@ -62,15 +62,6 @@
|
|||
"spoiler": {
|
||||
"click-to-show": "Spoiler, kliknij by wyświetlić"
|
||||
},
|
||||
"review-series-modal": {
|
||||
"title": "Edytuj recenzję",
|
||||
"review-label": "Recenzja",
|
||||
"close": "{{common.close}}",
|
||||
"save": "{{common.save}}",
|
||||
"delete": "{{common.delete}}",
|
||||
"min-length": "Recenzja musi zawierać co najmniej {{count}} znaków",
|
||||
"required": "{{validation.required-field}}"
|
||||
},
|
||||
"review-card-modal": {
|
||||
"close": "{{common.close}}",
|
||||
"user-review": "Recenzja {{username}}",
|
||||
|
|
@ -762,8 +753,6 @@
|
|||
"no-pages": "{{toasts.no-pages}}",
|
||||
"no-chapters": "W tym tomie nie ma rozdziałów. Nie można odczytać.",
|
||||
"cover-change": "Odświeżenie obrazu w przeglądarce może potrwać do minuty. Do tego czasu na niektórych stronach może być wyświetlany stary obraz.",
|
||||
"user-reviews-local": "Lokalne recenzje",
|
||||
"user-reviews-plus": "Zewnętrzne recenzje",
|
||||
"writers-title": "{{metadata-fields.writers-title}}",
|
||||
"cover-artists-title": "{{metadata-fields.cover-artists-title}}",
|
||||
"characters-title": "{{metadata-fields.characters-title}}",
|
||||
|
|
|
|||
|
|
@ -62,15 +62,6 @@
|
|||
"spoiler": {
|
||||
"click-to-show": "Spoiler, clique para ver"
|
||||
},
|
||||
"review-series-modal": {
|
||||
"title": "Editar Crítica",
|
||||
"review-label": "Crítica",
|
||||
"close": "{{common.close}}",
|
||||
"save": "{{common.save}}",
|
||||
"delete": "{{common.delete}}",
|
||||
"min-length": "A resenha deve ter pelo menos {{count}} caracteres",
|
||||
"required": "{{validation.required-field}}"
|
||||
},
|
||||
"review-card-modal": {
|
||||
"close": "{{common.close}}",
|
||||
"user-review": "Crítica de {{username}}",
|
||||
|
|
@ -762,8 +753,6 @@
|
|||
"no-pages": "{{toasts.no-pages}}",
|
||||
"no-chapters": "Não existem capítulos neste volume. Impossível ler.",
|
||||
"cover-change": "Pode levar até um minuto para a imagem ser refrescada pelo browser. Até isso acontecer, a imagem antiga será mostrada nalgumas páginas.",
|
||||
"user-reviews-local": "Críticas Locais",
|
||||
"user-reviews-plus": "Críticas Externas",
|
||||
"writers-title": "{{metadata-fields.writers-title}}",
|
||||
"cover-artists-title": "{{metadata-fields.cover-artists-title}}",
|
||||
"characters-title": "{{metadata-fields.characters-title}}",
|
||||
|
|
|
|||
|
|
@ -62,15 +62,6 @@
|
|||
"spoiler": {
|
||||
"click-to-show": "Spoiler, clique para mostrar"
|
||||
},
|
||||
"review-series-modal": {
|
||||
"title": "Editar Análise",
|
||||
"review-label": "Análise",
|
||||
"close": "{{common.close}}",
|
||||
"save": "{{common.save}}",
|
||||
"delete": "{{common.delete}}",
|
||||
"min-length": "A análise deve ter pelo menos {{count}} caracteres",
|
||||
"required": "{{validation.required-field}}"
|
||||
},
|
||||
"review-card-modal": {
|
||||
"close": "{{common.close}}",
|
||||
"user-review": "{{username}}'s Análise",
|
||||
|
|
@ -81,7 +72,8 @@
|
|||
"your-review": "Esta é sua análise",
|
||||
"external-review": "Análise Externa",
|
||||
"local-review": "Análise Local",
|
||||
"rating-percentage": "Avaliação {{r}}%"
|
||||
"rating-percentage": "Avaliação {{r}}%",
|
||||
"critic": "crítico"
|
||||
},
|
||||
"want-to-read": {
|
||||
"title": "Quero Ler",
|
||||
|
|
@ -762,8 +754,6 @@
|
|||
"no-pages": "{{toasts.no-pages}}",
|
||||
"no-chapters": "Não há capítulos neste volume. Não posso ler.",
|
||||
"cover-change": "Pode levar até um minuto para o seu navegador atualizar a imagem. Até lá, a imagem antiga pode ser exibida em algumas páginas.",
|
||||
"user-reviews-local": "Análises Locais",
|
||||
"user-reviews-plus": "Análises Externas",
|
||||
"writers-title": "{{metadata-fields.writers-title}}",
|
||||
"cover-artists-title": "{{metadata-fields.cover-artists-title}}",
|
||||
"characters-title": "{{metadata-fields.characters-title}}",
|
||||
|
|
@ -825,7 +815,8 @@
|
|||
"entry-label": "Ver Detalhes",
|
||||
"kavita-tooltip": "Sua Avaliação + Geral",
|
||||
"kavita-rating-title": "Sua Avaliação",
|
||||
"close": "{{common.close}}"
|
||||
"close": "{{common.close}}",
|
||||
"critic": "{{review-card.critic}}"
|
||||
},
|
||||
"badge-expander": {
|
||||
"more-items": "e {{count}} mais"
|
||||
|
|
@ -1522,7 +1513,8 @@
|
|||
"logout": "Sair",
|
||||
"all-filters": "Filtros Inteligentes",
|
||||
"nav-link-header": "Opções de Navegação",
|
||||
"close": "{{common.close}}"
|
||||
"close": "{{common.close}}",
|
||||
"person-aka-status": "Resultados como pseudônimos"
|
||||
},
|
||||
"promoted-icon": {
|
||||
"promoted": "{{common.promoted}}"
|
||||
|
|
@ -2261,7 +2253,8 @@
|
|||
"match-success": "Série correspondida corretamente",
|
||||
"webtoon-override": "Mudando para o modo Webtoon devido a imagens que representam um Webtoon.",
|
||||
"scrobble-gen-init": "Enfileirou uma tarefa para gerar eventos scrobble a partir do histórico de leitura e avaliações anteriores, sincronizando-os com serviços conectados.",
|
||||
"confirm-delete-multiple-volumes": "Tem certeza de que deseja excluir {{count}} volumes? Isso não modificará os arquivos no disco."
|
||||
"confirm-delete-multiple-volumes": "Tem certeza de que deseja excluir {{count}} volumes? Isso não modificará os arquivos no disco.",
|
||||
"series-added-want-to-read": "Série adicionada da lista Quero Ler"
|
||||
},
|
||||
"read-time-pipe": {
|
||||
"less-than-hour": "<1 Hora",
|
||||
|
|
@ -2336,7 +2329,8 @@
|
|||
"match-tooltip": "Corresponder Séries com Kavita+ manualmente",
|
||||
"reorder": "Reordenar",
|
||||
"rename-tooltip": "Renomear o Filtro Inteligente",
|
||||
"rename": "Renomear"
|
||||
"rename": "Renomear",
|
||||
"merge": "Mesclar"
|
||||
},
|
||||
"preferences": {
|
||||
"left-to-right": "Esquerda para Direita",
|
||||
|
|
@ -2485,7 +2479,11 @@
|
|||
"cover-image-description-extra": "Alternativamente, você pode baixar uma capa do CoversDB, se disponível.",
|
||||
"download-coversdb": "Baixar do CoversDB",
|
||||
"hardcover-tooltip": "https://hardcover.app/authors/{HardcoverId}",
|
||||
"asin-tooltip": "https://www.amazon.com/stores/J.K.-Rowling/author/{ASIN}"
|
||||
"asin-tooltip": "https://www.amazon.com/stores/J.K.-Rowling/author/{ASIN}",
|
||||
"alias-overlap": "Esse pseudônimo já aponta para outra pessoa ou é o nome dessa pessoa, considere mesclá -la.",
|
||||
"aliases-label": "Editar pseudônimos",
|
||||
"aliases-tab": "Pseudônimos",
|
||||
"aliases-tooltip": "Quando uma série é etiquetada com um pseudônimo de uma pessoa, a pessoa é atribuída em vez de ser criada uma nova pessoa. Ao eliminar um pseudônimo, terá de voltar a analisar a série para que a alteração seja detectada."
|
||||
},
|
||||
"browse-authors": {
|
||||
"author-count": "{{num}} Pessoas",
|
||||
|
|
@ -2498,7 +2496,9 @@
|
|||
"individual-role-title": "Como um {{role}}",
|
||||
"known-for-title": "Conhecido Por",
|
||||
"all-roles": "Papéis",
|
||||
"anilist-url": "{{edit-person-modal.anilist-tooltip}}"
|
||||
"anilist-url": "{{edit-person-modal.anilist-tooltip}}",
|
||||
"no-info": "Nenhuma informação sobre esta Pessoa",
|
||||
"aka-title": "Também conhecido como · "
|
||||
},
|
||||
"manage-user-tokens": {
|
||||
"anilist-header": "AniList",
|
||||
|
|
@ -2561,7 +2561,7 @@
|
|||
"save": "{{common.save}}",
|
||||
"no-results": "Não foi possível encontrar uma correspondência. Tente adicionar o URL de um provedor compatível e tente novamente.",
|
||||
"query-label": "Consulta",
|
||||
"query-tooltip": "Digite o nome da série, URL AniList/MyAnimeList. As URLs usarão uma pesquisa direta.",
|
||||
"query-tooltip": "Digite o nome da série, urls de Anilist/Myanimelist/ComicbookRoundup URL. As URLs serão usadas como pesquisa direta.",
|
||||
"dont-match-label": "Não Fazer Correspondência",
|
||||
"dont-match-tooltip": "Opte por esta série de correspondência e scrobbling",
|
||||
"search": "Pesquisar"
|
||||
|
|
@ -2571,7 +2571,8 @@
|
|||
"chapter-count": "{{common.chapter-count}}",
|
||||
"releasing": "Lançando",
|
||||
"details": "Exibir página",
|
||||
"updating-metadata-status": "Atualizando Metadados"
|
||||
"updating-metadata-status": "Atualizando Metadados",
|
||||
"issue-count": "{{common.issue-count}}"
|
||||
},
|
||||
"email-history": {
|
||||
"description": "Aqui você encontra todos os e-mails enviados por Kavita e para qual usuário.",
|
||||
|
|
@ -2622,7 +2623,18 @@
|
|||
"enable-cover-image-tooltip": "Permitir que Kavita escreva a imagem da capa das Séries",
|
||||
"localized-name-tooltip": "Permitir que o nome localizado seja escrito quando o campo for desbloqueado. Kavita tentará dar o melhor palpite.",
|
||||
"overrides-description": "Permita que Kavita escreva os campos trancados.",
|
||||
"overrides-label": "Substituir"
|
||||
"overrides-label": "Substituir",
|
||||
"enable-chapter-title-label": "Título",
|
||||
"enable-chapter-publisher-label": "Editora",
|
||||
"enable-chapter-publisher-tooltip": "Permitir que a Editora do Capítulo/Número seja salvo",
|
||||
"enable-chapter-cover-label": "Capa do Capítulo",
|
||||
"enable-chapter-cover-tooltip": "Permitir que a Capa do Capítulo/Número seja definida",
|
||||
"chapter-header": "Campos de Capítulo",
|
||||
"enable-chapter-title-tooltip": "Permitir que o Título do Capítulo/Número seja salvo",
|
||||
"enable-chapter-summary-label": "{{manage-metadata-settings.summary-label}}",
|
||||
"enable-chapter-summary-tooltip": "{{manage-metadata-settings.summary-tooltip}}",
|
||||
"enable-chapter-release-date-label": "Data de Lançamento",
|
||||
"enable-chapter-release-date-tooltip": "Permitir que a Data de Lançamento do Capítulo/Número seja salvo"
|
||||
},
|
||||
"metadata-setting-field-pipe": {
|
||||
"publication-status": "{{edit-series-modal.publication-status-title}}",
|
||||
|
|
@ -2633,7 +2645,12 @@
|
|||
"localized-name": "{{edit-series-modal.localized-name-label}}",
|
||||
"age-rating": "{{metadata-fields.age-rating-title}}",
|
||||
"people": "{{tabs.people-tab}}",
|
||||
"summary": "{{filter-field-pipe.summary}}"
|
||||
"summary": "{{filter-field-pipe.summary}}",
|
||||
"chapter-summary": "Resumo (Capítulo)",
|
||||
"chapter-covers": "Capas (Capítulo)",
|
||||
"chapter-publisher": "{{person-role-pipe.publisher}} (Capítulo)",
|
||||
"chapter-title": "Título (Capítulo)",
|
||||
"chapter-release-date": "Data de Lançamento (Capítulo)"
|
||||
},
|
||||
"role-localized-pipe": {
|
||||
"admin": "Admin",
|
||||
|
|
@ -2651,5 +2668,27 @@
|
|||
"trace": "Rastro",
|
||||
"warning": "Aviso",
|
||||
"critical": "Crítico"
|
||||
},
|
||||
"reviews": {
|
||||
"user-reviews-local": "Análises Locais",
|
||||
"user-reviews-plus": "Análises Externas"
|
||||
},
|
||||
"review-modal": {
|
||||
"title": "Editar Análise",
|
||||
"review-label": "Análise",
|
||||
"close": "{{common.close}}",
|
||||
"delete": "{{common.delete}}",
|
||||
"min-length": "A análise deve ter pelo menos {{count}} caracteres",
|
||||
"required": "{{validation.required-field}}",
|
||||
"save": "{{common.save}}"
|
||||
},
|
||||
"merge-person-modal": {
|
||||
"save": "{{common.save}}",
|
||||
"src": "Mesclar Pessoa",
|
||||
"alias-title": "Novos pseudônimos",
|
||||
"close": "{{common.close}}",
|
||||
"title": "{{personName}}",
|
||||
"merge-warning": "se prosseguir, a pessoa selecionada será removida. O nome da pessoa selecionada será adicionado como um pseudônimo e todas as suas funções serão transferidas.",
|
||||
"known-for-title": "Conhecido por"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,15 +59,6 @@
|
|||
"spoiler": {
|
||||
"click-to-show": "Спойлер, нажмите, чтобы показать"
|
||||
},
|
||||
"review-series-modal": {
|
||||
"title": "Редактировать отзыв",
|
||||
"review-label": "Обзор",
|
||||
"close": "{{common.close}}",
|
||||
"save": "{{common.save}}",
|
||||
"delete": "{{common.delete}}",
|
||||
"min-length": "Отзыв должен содержать не меньше {{count}} символов",
|
||||
"required": "{{validation.required-field}}"
|
||||
},
|
||||
"review-card-modal": {
|
||||
"close": "{{common.close}}",
|
||||
"user-review": "Обзор на {{username}}",
|
||||
|
|
|
|||
|
|
@ -62,15 +62,6 @@
|
|||
"spoiler": {
|
||||
"click-to-show": "Spojler, kliknite na zobrazenie"
|
||||
},
|
||||
"review-series-modal": {
|
||||
"title": "Upraviť recenziu",
|
||||
"review-label": "Recenzia",
|
||||
"close": "{{common.close}}",
|
||||
"save": "{{common.save}}",
|
||||
"delete": "{{common.delete}}",
|
||||
"required": "{{validation.required-field}}",
|
||||
"min-length": "Recenzia musí obsahovať aspoň {{count}} znakov"
|
||||
},
|
||||
"review-card-modal": {
|
||||
"close": "{{common.close}}",
|
||||
"external-mod": "(externé)",
|
||||
|
|
@ -784,8 +775,6 @@
|
|||
"more-alt": "Viac",
|
||||
"publication-status-title": "Publikácia",
|
||||
"publication-status-tooltip": "Stav publikácie",
|
||||
"user-reviews-local": "Miestne recenzie",
|
||||
"user-reviews-plus": "Externé recenzie",
|
||||
"pages-count": "{{num}} Strán",
|
||||
"words-count": "{{num}} Slov",
|
||||
"weblinks-title": "Odkazy",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"username": "{{common.username}}",
|
||||
"password": "{{common.password}}",
|
||||
"password-validation": "{{validation.password-validation}}",
|
||||
"title": "Logga in till ditt konto",
|
||||
"title": "Logga in på ditt konto",
|
||||
"forgot-password": "Glömt lösenord?",
|
||||
"submit": "Logga in"
|
||||
},
|
||||
|
|
@ -43,15 +43,6 @@
|
|||
"token-expired": "Din AniList-token är Utgången! Scrobbling kommer inte processeras förrän du har förnyat det under Konto.",
|
||||
"generate-scrobble-events": "Backfill Events"
|
||||
},
|
||||
"review-series-modal": {
|
||||
"close": "{{common.close}}",
|
||||
"save": "{{common.save}}",
|
||||
"delete": "{{common.delete}}",
|
||||
"required": "{{validation.required-field}}",
|
||||
"title": "Redigera recension",
|
||||
"review-label": "Recension",
|
||||
"min-length": "Recension måste vara minst {{count}} tecken"
|
||||
},
|
||||
"review-card-modal": {
|
||||
"close": "{{common.close}}",
|
||||
"external-mod": "(extern)",
|
||||
|
|
@ -86,7 +77,7 @@
|
|||
"locale-label": "Språk",
|
||||
"locale-tooltip": "Språket Kavita ska nyttja",
|
||||
"blur-unread-summaries-label": "Censurera olästa sammanfattningar",
|
||||
"blur-unread-summaries-tooltip": "Cencurera sammanfattningar på volymer eller kapitel som inte har någon läsprogress (för att undvika spoilers)",
|
||||
"blur-unread-summaries-tooltip": "Cencurera handlingen på volymer eller kapitel som inte har någon läsprogress (för att undvika spoilers)",
|
||||
"disable-animations-tooltip": "Inaktivera animationer på sidan. Användbart på e-ink-läsare.",
|
||||
"reading-mode-label": "Läsläge",
|
||||
"layout-mode-label": "Layoutläge",
|
||||
|
|
@ -269,7 +260,7 @@
|
|||
"age-restriction-label": "Åldersbegränsning"
|
||||
},
|
||||
"scrobbling-providers": {
|
||||
"instructions": "First time users should click on \"{{scrobbling-providers.generate}}\" below to allow Kavita+ to talk with {{service}}. Once you authorize the program, copy and paste the token in the input below. You can regenerate your token at any time.",
|
||||
"instructions": "Nya användare bör klicka på \"{{scrobbling-providers.generate}}\" nedan för att tillåta Kavita+ att prata med {{service}}. Efter att du gett programmet åtkomst, kopiera och klistra in token i rutan nedanför. Du kan generera nytt token när som helst.",
|
||||
"edit": "{{common.edit}}",
|
||||
"cancel": "{{common.cancel}}",
|
||||
"save": "{{common.save}}",
|
||||
|
|
@ -504,7 +495,7 @@
|
|||
"incognito-mode-label": "Inkognitoläge",
|
||||
"next": "Nästa",
|
||||
"next-chapter": "Nästa Kapitel/Volym",
|
||||
"close-reader": "Stäng läsare",
|
||||
"close-reader": "Stäng Läsare",
|
||||
"go-to-page": "Gå till sida",
|
||||
"go-to-last-page": "Gå till sista sidan",
|
||||
"prev-chapter": "Förra Kapitel/Volym",
|
||||
|
|
@ -601,8 +592,6 @@
|
|||
"cover-change": "Det kan ta upp till en minut för att din webbläsare ska uppdatera bilden. Under tiden kan den gamla bilden visas på vissa sidor.",
|
||||
"layout-mode-option-list": "Lista",
|
||||
"read-incognito": "Läs inkognito",
|
||||
"user-reviews-plus": "Externa recensioner",
|
||||
"user-reviews-local": "Lokala recensioner",
|
||||
"continue-incognito": "Fortsätt inkognito",
|
||||
"incognito": "Inkognito",
|
||||
"continue-from": "Fortsätt {{title}}",
|
||||
|
|
@ -636,7 +625,7 @@
|
|||
"editors-title": "Redaktörer",
|
||||
"colorists-title": "Färgläggare",
|
||||
"letterers-title": "Bokstavsättare",
|
||||
"inkers-title": "Inker"
|
||||
"inkers-title": "Inkers"
|
||||
},
|
||||
"external-rating": {
|
||||
"close": "{{common.close}}",
|
||||
|
|
@ -944,7 +933,7 @@
|
|||
"bookmark-dir-tooltip": "Plats där bokmärken kommer att lagras. Bokmärken är källfiler och kan vara stora. Välj en plats med tillräckligt med lagring. Katalogen hanteras; andra filer inom katalogen kommer att raderas. Om du använder Docker, montera en extra volym och använd den.",
|
||||
"encode-as-description-part-2": "Kan jag använda WebP?",
|
||||
"media-issue-title": "Medieproblem",
|
||||
"scrobble-issue-title": "Scrobble-problem",
|
||||
"scrobble-issue-title": "Scrobble Nummer",
|
||||
"cover-image-size-label": "Omslagsbildsstorlek"
|
||||
},
|
||||
"manage-scrobble-errors": {
|
||||
|
|
@ -1110,7 +1099,7 @@
|
|||
"filter-label": "{{common.filter}}",
|
||||
"promote-tooltip": "Marknadsför betyder att taggen kan ses over hela servern, inte bara för admin-användare. Alla serier som har denna tagg kommer fortfarande att ha användarbegränsningar placerade på sig.",
|
||||
"title": "Redigera {{collectionName}} Samling",
|
||||
"summary-label": "Summering",
|
||||
"summary-label": "Handling",
|
||||
"missing-series-title": "Saknade Serier:",
|
||||
"name-validation": "Namn måste vara unikt",
|
||||
"name-label": "Namn",
|
||||
|
|
@ -1295,7 +1284,7 @@
|
|||
"close": "{{common.close}}",
|
||||
"save": "{{common.save}}",
|
||||
"required-field": "{{validation.required-field}}",
|
||||
"summary-label": "Summering",
|
||||
"summary-label": "Handling",
|
||||
"promote-label": "Tipsa",
|
||||
"promote-tooltip": "Tipsa betyder att samlingen kan ses över hela servern, inte bara av dig. Alla serier inom denna samling kommer fortfarande att ha användarbegränsningar som gäller för dem.",
|
||||
"starting-title": "Startar",
|
||||
|
|
@ -1494,7 +1483,7 @@
|
|||
"highest-count-tooltip": "Högsta Antal funnet i alla ComicInfo i serien",
|
||||
"name-label": "Namn",
|
||||
"sort-name-label": "Sortera Namn",
|
||||
"summary-label": "Summering",
|
||||
"summary-label": "Handling",
|
||||
"total-words-title": "Totalt antal Ord",
|
||||
"size-title": "Storlek",
|
||||
"specials-volume": "Specials",
|
||||
|
|
@ -1667,7 +1656,7 @@
|
|||
"filter": "{{common.filter}}",
|
||||
"clear": "{{common.clear}}",
|
||||
"external-sources-title": "{{customize-dashboard-modal.external-sources}}",
|
||||
"reorder-when-filter-present": "You cannot reorder items via drag & drop while a filter is present. Use {{customize-sidenav-streams.order-numbers-label}}",
|
||||
"reorder-when-filter-present": "Du kan inte ordna om objekt via drag & släpp medan ett filter används. Använd {{customize-sidenav-streams.order-numbers-label}}",
|
||||
"order-numbers-label": "{{reading-list-detail.order-numbers-label}}",
|
||||
"bulk-mode-label": "Massläge",
|
||||
"no-data-external-source": "Alla Externa Källor har lagts till i Sido Nav eller så har inga skapats än.",
|
||||
|
|
@ -1752,7 +1741,7 @@
|
|||
"imprint": "Förlag",
|
||||
"average-rating": "Genomsnittligt betyg",
|
||||
"series-name": "Serie Namn",
|
||||
"summary": "Summering",
|
||||
"summary": "Handling",
|
||||
"read-date": "Läsdatum",
|
||||
"want-to-read": "Vill Läsa",
|
||||
"publication-status": "Publikationsstatus"
|
||||
|
|
@ -1823,13 +1812,15 @@
|
|||
"remove-from-on-deck-tooltip": "Radera serier från att visas på Hyllan",
|
||||
"add-to-collection-tooltip": "Lägg till serier i en samling",
|
||||
"delete-tooltip": "Det finns inget sätt att ångra detta beslut",
|
||||
"back-to": "Tillbaka till {{action}}"
|
||||
"back-to": "Tillbaka till {{action}}",
|
||||
"rename": "Byt namn",
|
||||
"rename-tooltip": "Byt namn på Smart Filter"
|
||||
},
|
||||
"dashboard": {
|
||||
"recently-added-title": "Nyligen tillagda serier",
|
||||
"more-in-genre-title": "Mer inom {{genre}}",
|
||||
"server-settings-link": "Serverinställningar",
|
||||
"no-libraries": "Det är inga bibliotek skapade än. Skapa några i",
|
||||
"no-libraries": "Det finns inga bibliotek skapade än. Skapa några i",
|
||||
"not-granted": "Du har inte fått rättigheter till något bibliotek.",
|
||||
"on-deck-title": "På hyllan",
|
||||
"recently-updated-title": "Nyligen uppdaterade serier"
|
||||
|
|
@ -1866,7 +1857,7 @@
|
|||
},
|
||||
"manage-metadata-settings": {
|
||||
"enabled-tooltip": "Tillåt Kavita att ladda ner metadata och skriva till sin databas.",
|
||||
"summary-label": "Summering",
|
||||
"summary-label": "Handling",
|
||||
"enable-people-label": "Personer",
|
||||
"enable-start-date-tooltip": "Tillåt Seriens startdatum att bli skriven till serien",
|
||||
"enable-genres-label": "Genres",
|
||||
|
|
@ -1887,7 +1878,7 @@
|
|||
"enable-genres-tooltip": "Tillåt serie-genrer att skrivas.",
|
||||
"description": "Kavita+ har möjlighet att ladda ner och skriva viss begränsad metadata till databasen. Denna sida gör det möjligt för dig att växla vad som ingår.",
|
||||
"enable-people-tooltip": "Tillåt personer (karaktärer, författare, osv.) att <b>läggas till</b>. Alla personer inkluderar bilder.",
|
||||
"summary-tooltip": "TIllåt Summering att skrivas när fältet är upplåst.",
|
||||
"summary-tooltip": "Tillåt Handling att skrivas när fältet är upplåst.",
|
||||
"derive-publication-status-tooltip": "Tillåt publiceringsstatus att härledas från totala kapitel-/volymantal.",
|
||||
"derive-publication-status-label": "Publiceringsstatus",
|
||||
"enable-relations-label": "Förhållanden",
|
||||
|
|
@ -1903,7 +1894,18 @@
|
|||
"enable-tags-label": "Taggar",
|
||||
"age-rating-mapping-title": "Åldersgruppsbedömning",
|
||||
"add-age-rating-mapping-label": "Lägg till Åldersgruppsbedömning",
|
||||
"field-mapping-title": "Fältmappning"
|
||||
"field-mapping-title": "Fältmappning",
|
||||
"enable-chapter-title-label": "Titel",
|
||||
"enable-chapter-summary-label": "{{manage-metadata-settings.summary-label}}",
|
||||
"enable-chapter-title-tooltip": "Tillåt Titeln för Kapitel/Nummer att skrivas",
|
||||
"enable-chapter-cover-tooltip": "Tillåt Omslag för Kapitel/Nummer att ställas in",
|
||||
"enable-chapter-cover-label": "Kapitelomslag",
|
||||
"enable-chapter-summary-tooltip": "{{manage-metadata-settings.summary-tooltip}}",
|
||||
"enable-chapter-release-date-label": "Släppdatum",
|
||||
"enable-chapter-release-date-tooltip": "Tillåt Släppdatum för Kapitel/Nummer att skrivas",
|
||||
"enable-chapter-publisher-tooltip": "Tillåt Utgivare för Kapitel/Nummer att skrivas",
|
||||
"enable-chapter-publisher-label": "Utgivare",
|
||||
"chapter-header": "Kapitelfält"
|
||||
},
|
||||
"match-series-result-item": {
|
||||
"releasing": "Släpper",
|
||||
|
|
@ -2129,7 +2131,7 @@
|
|||
"show": "Visa",
|
||||
"hide": "Göm",
|
||||
"regen-warning": "Att regenerera din API-nyckel kommer att ogiltigförklara alla befintliga klienter.",
|
||||
"no-key": "FEL – NYCKEL INTE ANGETTS",
|
||||
"no-key": "ERROR – NYCKEL HAR EJ ANGETTS",
|
||||
"confirm-reset": "Detta kommer att ogiltigförklara alla OPDS-konfigurationer du har ställt in. Är du säker på att du vill fortsätta?",
|
||||
"key-reset": "Återställ API-nyckel",
|
||||
"copy": "Kopiera"
|
||||
|
|
@ -2168,7 +2170,7 @@
|
|||
"sunday": "Söndag"
|
||||
},
|
||||
"device-platform-pipe": {
|
||||
"custom": "Anpassa"
|
||||
"custom": "Anpassad"
|
||||
},
|
||||
"cbl-import-result-pipe": {
|
||||
"success": "Lyckats",
|
||||
|
|
@ -2648,6 +2650,11 @@
|
|||
"start-date": "{{manage-metadata-settings.enable-start-date-label}}",
|
||||
"genres": "{{metadata-fields.genres-title}}",
|
||||
"tags": "{{metadata-fields.tags-title}}",
|
||||
"localized-name": "{{edit-series-modal.localized-name-label}}"
|
||||
"localized-name": "{{edit-series-modal.localized-name-label}}",
|
||||
"chapter-summary": "Handling (Kapitel)",
|
||||
"chapter-publisher": "{{person-role-pipe.publisher}} (Kapitel)",
|
||||
"chapter-title": "Titel (Kapitel)",
|
||||
"chapter-release-date": "Släppdatum (Kapitel)",
|
||||
"chapter-covers": "Omslag (Kapitel)"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,15 +75,6 @@
|
|||
"spoiler": {
|
||||
"click-to-show": "ச்பாய்லர், காட்ட சொடுக்கு செய்க"
|
||||
},
|
||||
"review-series-modal": {
|
||||
"title": "மதிப்பாய்வைத் திருத்தவும்",
|
||||
"review-label": "சீராய்வு",
|
||||
"close": "{{common.close}}",
|
||||
"save": "{{common.save}}",
|
||||
"delete": "{{common.delete}}",
|
||||
"min-length": "மதிப்பாய்வு குறைந்தபட்சம் {{count}} எழுத்துக்களாக இருக்க வேண்டும்",
|
||||
"required": "{{validation.required-field}}"
|
||||
},
|
||||
"theme-manager": {
|
||||
"set-default": "இயல்புநிலையை அமைக்கவும்",
|
||||
"default-theme": "இயல்புநிலை",
|
||||
|
|
@ -237,7 +228,6 @@
|
|||
"kavita+-requirement": "கவிதா+ அண்மைக் கால வெளியீடு - 2 பதிப்புகளுடன் மட்டுமே வேலை செய்ய வடிவமைக்கப்பட்டுள்ளது. அதற்கு வெளியே எதுவும் வேலை செய்யாததற்கு உட்பட்டது."
|
||||
},
|
||||
"series-detail": {
|
||||
"user-reviews-plus": "வெளிப்புற மதிப்புரைகள்",
|
||||
"writers-title": "{{metadata-fields.writers-title}}}",
|
||||
"cover-artists-title": "{{metadata-fields.cover-artists-title}}}",
|
||||
"characters-title": "{{metadata-fields.Characters-title}}}",
|
||||
|
|
@ -251,7 +241,6 @@
|
|||
"tags-title": "{{metadata-fields.tags-title}}}",
|
||||
"ongoing": "{{வெளியீட்டு-நிலை-பைப்.ங்கோயிங்}}",
|
||||
"release-date-title": "வெளியீடு",
|
||||
"user-reviews-local": "உள்ளக மதிப்புரைகள்",
|
||||
"page-settings-title": "பக்க அமைப்புகள்",
|
||||
"close": "{{common.close}}",
|
||||
"layout-mode-label": "{{பயனர்-முன்னுரிமைகள்.லேவுட்-மோட்-புக்-லேபிள்}}}",
|
||||
|
|
|
|||
|
|
@ -55,14 +55,6 @@
|
|||
"spoiler": {
|
||||
"click-to-show": "คลิกเพื่อแสดง"
|
||||
},
|
||||
"review-series-modal": {
|
||||
"title": "แก้ไขรีวิว",
|
||||
"review-label": "รีวิว",
|
||||
"close": "{{common.close}}",
|
||||
"save": "{{common.save}}",
|
||||
"delete": "{{common.delete}}",
|
||||
"required": "{{validation.required-field}}"
|
||||
},
|
||||
"review-card-modal": {
|
||||
"close": "{{common.close}}",
|
||||
"user-review": "รีวิวของ {{username}}",
|
||||
|
|
|
|||
|
|
@ -49,14 +49,6 @@
|
|||
"spoiler": {
|
||||
"click-to-show": "Spoiler, görmek için tıklayın"
|
||||
},
|
||||
"review-series-modal": {
|
||||
"title": "İncelemeyi düzenle",
|
||||
"review-label": "İnceleme",
|
||||
"close": "{{common.close}}",
|
||||
"save": "{{common.save}}",
|
||||
"delete": "{{common.delete}}",
|
||||
"required": "{{validation.required-field}}"
|
||||
},
|
||||
"review-card-modal": {
|
||||
"close": "{{common.close}}",
|
||||
"user-review": "{{username}} İncelemesi",
|
||||
|
|
@ -122,7 +114,9 @@
|
|||
"clients-api-key-tooltip": "API anahtarı bir şifre gibidir. Sıfırlamak mevcut istemcileri geçersiz kılar.",
|
||||
"clients-opds-url-tooltip": "Desteklenen OPDS istemcilerinin listesine bakın: ",
|
||||
"reset": "{{common.reset}}",
|
||||
"save": "{{common.save}}"
|
||||
"save": "{{common.save}}",
|
||||
"pdf-reader-settings-title": "PDF okuyucu",
|
||||
"kavitaplus-settings-title": "Kavita+"
|
||||
},
|
||||
"user-holds": {
|
||||
"no-data": "{{typeahead.no-data}}"
|
||||
|
|
@ -138,7 +132,9 @@
|
|||
"delete": "{{common.delete}}",
|
||||
"drag-n-drop": "{{cover-image-chooser.drag-n-drop}}",
|
||||
"upload": "{{cover-image-chooser.upload}}",
|
||||
"add": "{{common.add}}"
|
||||
"add": "{{common.add}}",
|
||||
"downloadable": "İndirilebilir",
|
||||
"downloaded": "İndirildi"
|
||||
},
|
||||
"theme": {
|
||||
"theme-dark": "Karanlık",
|
||||
|
|
@ -183,7 +179,10 @@
|
|||
"edit": "{{common.edit}}",
|
||||
"cancel": "{{common.cancel}}",
|
||||
"save": "{{common.save}}",
|
||||
"required-field": "{{validation.required-field}}"
|
||||
"required-field": "{{validation.required-field}}",
|
||||
"confirm-password-label": "Parola onayı",
|
||||
"current-password-label": "Geçerli parola",
|
||||
"new-password-label": "Yeni parola"
|
||||
},
|
||||
"change-email": {
|
||||
"required-field": "{{validation.required-field}}",
|
||||
|
|
@ -192,7 +191,10 @@
|
|||
"edit": "{{common.edit}}",
|
||||
"cancel": "{{common.cancel}}",
|
||||
"save": "{{common.save}}",
|
||||
"setup-user-account": "Kullanıcının hesabını kur"
|
||||
"setup-user-account": "Kullanıcının hesabını kur",
|
||||
"email-title": "E-posta",
|
||||
"email-label": "Yeni e-posta",
|
||||
"current-password-label": "Geçerli parola"
|
||||
},
|
||||
"change-age-restriction": {
|
||||
"reset": "{{common.reset}}",
|
||||
|
|
@ -258,12 +260,16 @@
|
|||
"email-label": "{{common.email}}",
|
||||
"required-field": "{{validation.required-field}}",
|
||||
"valid-email": "{{validation.valid-email}}",
|
||||
"submit": "{{common.submit}}"
|
||||
"submit": "{{common.submit}}",
|
||||
"title": "Parolayı sıfırla"
|
||||
},
|
||||
"reset-password-modal": {
|
||||
"close": "{{common.close}}",
|
||||
"cancel": "{{common.cancel}}",
|
||||
"save": "{{common.save}}"
|
||||
"save": "{{common.save}}",
|
||||
"title": "{{username}}'ın parolasını sıfırla",
|
||||
"new-password-label": "Yeni parola",
|
||||
"error-label": "Hata: "
|
||||
},
|
||||
"all-series": {
|
||||
"series-count": "{{common.series-count}}"
|
||||
|
|
@ -295,7 +301,8 @@
|
|||
"activate-email-label": "{{common.email}}",
|
||||
"activate-delete": "{{common.delete}}",
|
||||
"activate-reset": "Lisansı Sıfırla",
|
||||
"activate-save": "{{common.save}}"
|
||||
"activate-save": "{{common.save}}",
|
||||
"reset-label": "Sıfırla"
|
||||
},
|
||||
"book-line-overlay": {
|
||||
"close": "{{common.close}}",
|
||||
|
|
@ -317,7 +324,9 @@
|
|||
"password-label": "{{common.password}}",
|
||||
"required-field": "{{validation.required-field}}",
|
||||
"submit": "{{common.submit}}",
|
||||
"password-validation": "{{validation.password-validation}}"
|
||||
"password-validation": "{{validation.password-validation}}",
|
||||
"description": "Yeni parola girin",
|
||||
"title": "Parolayı sıfırla"
|
||||
},
|
||||
"register": {
|
||||
"username-label": "{{common.username}}",
|
||||
|
|
@ -362,7 +371,7 @@
|
|||
"volume-num": "{{common.volume-num}}",
|
||||
"reading-lists-title": "{{side-nav.reading-lists}}",
|
||||
"time-to-read-alt": "{{sort-field-pipe.time-to-read}}",
|
||||
"scrobbling-tooltip": "{{settings.scrobbling}}"
|
||||
"scrobbling-tooltip": "{{settings.scrobbling}}: {{value}}"
|
||||
},
|
||||
"metadata-fields": {
|
||||
"collections-title": "{{side-nav.collections}}",
|
||||
|
|
@ -393,7 +402,8 @@
|
|||
"cancel": "{{common.cancel}}",
|
||||
"save": "{{common.save}}",
|
||||
"required-field": "{{validation.required-field}}",
|
||||
"help": "{{common.help}}"
|
||||
"help": "{{common.help}}",
|
||||
"force-scan": "Taramaya zorla"
|
||||
},
|
||||
"reader-settings": {
|
||||
"font-family-label": "{{user-preferences.font-family-label}}",
|
||||
|
|
@ -403,7 +413,8 @@
|
|||
"reading-direction-label": "{{user-preferences.reading-direction-book-label}}",
|
||||
"writing-style-label": "{{user-preferences.writing-style-label}}",
|
||||
"immersive-mode-label": "{{user-preferences.immersive-mode-label}}",
|
||||
"layout-mode-label": "{{user-preferences.layout-mode-book-label}}"
|
||||
"layout-mode-label": "{{user-preferences.layout-mode-book-label}}",
|
||||
"reset-to-defaults": "Varsayılanlara sıfırla"
|
||||
},
|
||||
"bookmarks": {
|
||||
"title": "{{side-nav.bookmarks}}",
|
||||
|
|
@ -438,7 +449,8 @@
|
|||
"cover-image-chooser": {
|
||||
"reset": "{{common.reset}}",
|
||||
"apply": "{{common.apply}}",
|
||||
"applied": "{{theme-manager.applied}}"
|
||||
"applied": "{{theme-manager.applied}}",
|
||||
"reset-cover-tooltip": "Kapak görselini sıfırla"
|
||||
},
|
||||
"edit-series-relation": {
|
||||
"remove": "{{common.remove}}",
|
||||
|
|
@ -747,7 +759,8 @@
|
|||
"manage-smart-filters": {
|
||||
"delete": "{{common.delete}}",
|
||||
"filter": "{{common.filter}}",
|
||||
"clear": "{{common.clear}}"
|
||||
"clear": "{{common.clear}}",
|
||||
"cancel": "{{common.cancel}}"
|
||||
},
|
||||
"edit-external-source-item": {
|
||||
"save": "{{common.save}}",
|
||||
|
|
@ -777,7 +790,8 @@
|
|||
"writers": "{{metadata-fields.writers-title}}"
|
||||
},
|
||||
"actionable": {
|
||||
"clear": "{{common.clear}}"
|
||||
"clear": "{{common.clear}}",
|
||||
"scan-library-tooltip": "Değişiklikler için kütüphaneyi tarayın. Her klasörü kontrol etmek için taramayı zorla kullanın"
|
||||
},
|
||||
"preferences": {
|
||||
"1-column": "1 Sütun",
|
||||
|
|
@ -802,18 +816,30 @@
|
|||
"username": "Kullanıcı adı",
|
||||
"password": "Parola",
|
||||
"select-all": "Tümünü Seç",
|
||||
"book-num": "Kitap"
|
||||
"book-num": "Kitap",
|
||||
"cancel": "İptal",
|
||||
"reset-to-default": "Varsayılan olarak sıfırla"
|
||||
},
|
||||
"tabs": {
|
||||
"account-tab": "Hesap"
|
||||
},
|
||||
"toasts": {
|
||||
"account-registration-complete": "Hesap kaydı tamamlandı",
|
||||
"account-migration-complete": "Hesap taşıma tamamlandı"
|
||||
"account-migration-complete": "Hesap taşıma tamamlandı",
|
||||
"reset-ip-address": "IP adresleri sıfırla",
|
||||
"password-reset": "Parola sıfırla",
|
||||
"email-service-reset": "E-posta hizmeti sıfırla"
|
||||
},
|
||||
"api-key": {
|
||||
"hide": "Gizle",
|
||||
"show": "Göster",
|
||||
"copy": "Kopyala"
|
||||
"copy": "Kopyala",
|
||||
"reset": "Sıfırla"
|
||||
},
|
||||
"confirm": {
|
||||
"cancel": "{{common.cancel}}"
|
||||
},
|
||||
"publication-status-pipe": {
|
||||
"cancelled": "İptal edildi"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,15 +48,6 @@
|
|||
"not-processed": "Не опрацьовано",
|
||||
"chapter-num": "Розділ"
|
||||
},
|
||||
"review-series-modal": {
|
||||
"close": "{{common.close}}",
|
||||
"save": "{{common.save}}",
|
||||
"delete": "{{common.delete}}",
|
||||
"required": "{{validation.required-field}}",
|
||||
"title": "Редагувати огляд",
|
||||
"review-label": "Огляд",
|
||||
"min-length": "Огляд мусить містити принаймні {{count}} знаків"
|
||||
},
|
||||
"review-card-modal": {
|
||||
"close": "{{common.close}}",
|
||||
"user-review": "Огляд від {{username}}",
|
||||
|
|
|
|||
|
|
@ -39,15 +39,6 @@
|
|||
"processed": "Đã Xử Lý",
|
||||
"not-processed": "Chưa Xử Lý"
|
||||
},
|
||||
"review-series-modal": {
|
||||
"close": "{{common.close}}",
|
||||
"save": "{{common.save}}",
|
||||
"delete": "{{common.delete}}",
|
||||
"required": "{{validation.required-field}}",
|
||||
"title": "Sửa Đánh Giá",
|
||||
"review-label": "Đánh Giá",
|
||||
"min-length": "Đánh giá phải dài ít nhất {{count}} ký tự"
|
||||
},
|
||||
"review-card-modal": {
|
||||
"close": "{{common.close}}",
|
||||
"go-to-review": "Tới Phần Đánh giá",
|
||||
|
|
@ -562,8 +553,6 @@
|
|||
"read-options-alt": "Tùy Chọn Đọc",
|
||||
"edit-series-alt": "Chỉnh Sửa Thông Tin",
|
||||
"send-to": "Tệp được gửi qua email tới {{deviceName}}",
|
||||
"user-reviews-local": "Đánh Giá Cục Bộ",
|
||||
"user-reviews-plus": "Đánh Giá Bên Ngoài",
|
||||
"release-date-title": "Phát hành",
|
||||
"pages-count": "{{num}} Trang",
|
||||
"words-count": "{{num}} Từ",
|
||||
|
|
|
|||
|
|
@ -62,15 +62,6 @@
|
|||
"spoiler": {
|
||||
"click-to-show": "剧透,点击显示"
|
||||
},
|
||||
"review-series-modal": {
|
||||
"title": "编辑评论",
|
||||
"review-label": "评论",
|
||||
"close": "{{common.close}}",
|
||||
"save": "{{common.save}}",
|
||||
"delete": "{{common.delete}}",
|
||||
"min-length": "评论必须至少有 {{count}} 个字符",
|
||||
"required": "{{validation.required-field}}"
|
||||
},
|
||||
"review-card-modal": {
|
||||
"close": "{{common.close}}",
|
||||
"user-review": "{{username}}的评论",
|
||||
|
|
@ -81,7 +72,8 @@
|
|||
"your-review": "我的评论",
|
||||
"external-review": "外部评论",
|
||||
"local-review": "本地评论",
|
||||
"rating-percentage": "评分 {{r}}%"
|
||||
"rating-percentage": "评分 {{r}}%",
|
||||
"critic": "评论家"
|
||||
},
|
||||
"want-to-read": {
|
||||
"title": "想读",
|
||||
|
|
@ -106,7 +98,7 @@
|
|||
"locale-label": "本地语言",
|
||||
"locale-tooltip": "Kavita 当前语言",
|
||||
"blur-unread-summaries-label": "模糊未读摘要",
|
||||
"blur-unread-summaries-tooltip": "模糊没有阅读进度的卷或章节的摘要文本(避免剧透)",
|
||||
"blur-unread-summaries-tooltip": "模糊没有阅读进度的卷或章节的内容简介文本(避免剧透)",
|
||||
"prompt-on-download-label": "下载时提示",
|
||||
"prompt-on-download-tooltip": "当下载大小超过 {{size}}MB 时提示",
|
||||
"disable-animations-label": "关闭动画",
|
||||
|
|
@ -762,8 +754,6 @@
|
|||
"no-pages": "{{toasts.no-pages}}",
|
||||
"no-chapters": "本卷没有章节,无法读取。",
|
||||
"cover-change": "刷新图片需要一点时间。在此期间,可能某些页面仍显示旧图片。",
|
||||
"user-reviews-local": "本地评论",
|
||||
"user-reviews-plus": "外部评论",
|
||||
"writers-title": "{{metadata-fields.writers-title}}",
|
||||
"cover-artists-title": "{{metadata-fields.cover-artists-title}}",
|
||||
"characters-title": "{{metadata-fields.characters-title}}",
|
||||
|
|
@ -825,7 +815,8 @@
|
|||
"entry-label": "查看详情",
|
||||
"kavita-tooltip": "您的评分 + 总体",
|
||||
"kavita-rating-title": "您的评分",
|
||||
"close": "{{common.close}}"
|
||||
"close": "{{common.close}}",
|
||||
"critic": "{{review-card.critic}}"
|
||||
},
|
||||
"badge-expander": {
|
||||
"more-items": "和{{count}}个更多"
|
||||
|
|
@ -1522,7 +1513,8 @@
|
|||
"logout": "注销",
|
||||
"all-filters": "智能筛选",
|
||||
"nav-link-header": "导航选项",
|
||||
"close": "{{common.close}}"
|
||||
"close": "{{common.close}}",
|
||||
"person-aka-status": "匹配别名"
|
||||
},
|
||||
"promoted-icon": {
|
||||
"promoted": "{{common.promoted}}"
|
||||
|
|
@ -2261,7 +2253,8 @@
|
|||
"match-success": "系列匹配正确",
|
||||
"webtoon-override": "由于图片代表网络漫画,因此切换到网络漫画模式。",
|
||||
"scrobble-gen-init": "将一项任务加入队列,该任务将根据过去的阅读历史和评分生成 scrobble 事件,并将其与已连接的服务同步。",
|
||||
"confirm-delete-multiple-volumes": "您确定要删除这 {{count}} 个卷吗?这不会修改磁盘上的文件。"
|
||||
"confirm-delete-multiple-volumes": "您确定要删除这 {{count}} 个卷吗?这不会修改磁盘上的文件。",
|
||||
"series-added-want-to-read": "从“想读”列表中添加的系列"
|
||||
},
|
||||
"read-time-pipe": {
|
||||
"less-than-hour": "<1 小时",
|
||||
|
|
@ -2336,7 +2329,8 @@
|
|||
"match-tooltip": "使用 Kavita+ 手动匹配系列",
|
||||
"reorder": "重新排序",
|
||||
"rename-tooltip": "重命名智能筛选",
|
||||
"rename": "重命名"
|
||||
"rename": "重命名",
|
||||
"merge": "合并"
|
||||
},
|
||||
"preferences": {
|
||||
"left-to-right": "从左到右",
|
||||
|
|
@ -2485,7 +2479,11 @@
|
|||
"cover-image-description-extra": "或者,您可以从 CoversDB 下载封面(如果有)。",
|
||||
"download-coversdb": "从 CoversDB 下载",
|
||||
"mal-tooltip": "https://myanimelist.net/people/{MalId}/",
|
||||
"hardcover-tooltip": "https://hardcover.app/authors/{HardcoverId}"
|
||||
"hardcover-tooltip": "https://hardcover.app/authors/{HardcoverId}",
|
||||
"aliases-tooltip": "当某个系列被标记为某个人的别名时,系统会分配该人,而不是创建新的人。删除别名后,您必须重新扫描该系列才能生效。",
|
||||
"aliases-tab": "别名",
|
||||
"aliases-label": "编辑别名",
|
||||
"alias-overlap": "此别名已经指向另一个人或为该人的名字,请考虑合并它们。"
|
||||
},
|
||||
"person-detail": {
|
||||
"all-roles": "角色",
|
||||
|
|
@ -2493,7 +2491,9 @@
|
|||
"individual-role-title": "作为 {{role}}",
|
||||
"browse-person-title": "{{name}} 的全部作品",
|
||||
"browse-person-by-role-title": "{{name}} 作为 {{role}} 的所有作品",
|
||||
"anilist-url": "{{edit-person-modal.anilist-tooltip}}"
|
||||
"anilist-url": "{{edit-person-modal.anilist-tooltip}}",
|
||||
"no-info": "没有关于此人的信息",
|
||||
"aka-title": "又名 "
|
||||
},
|
||||
"browse-authors": {
|
||||
"author-count": "{{num}} 人",
|
||||
|
|
@ -2507,7 +2507,7 @@
|
|||
"description": "选择一个匹配项来重新连接 Kavita+ 元数据并重新生成 scrobble 事件。 不匹配可用于限制 Kavita 匹配元数据和乱序。",
|
||||
"no-results": "无法找到匹配项。尝试添加来自受支持的提供商的 URL,然后重试。",
|
||||
"query-label": "询问",
|
||||
"query-tooltip": "输入系列名称、AniList/MyAnimeList url。 URL 将使用直接查找。",
|
||||
"query-tooltip": "输入系列名称、AniList/MyAnimeList/ComicBookRoundup 网址。网址将使用直接查找。",
|
||||
"dont-match-label": "不匹配",
|
||||
"dont-match-tooltip": "从匹配和 scrobbling 中选择该系列",
|
||||
"search": "搜索"
|
||||
|
|
@ -2571,7 +2571,8 @@
|
|||
"chapter-count": "{{common.chapter-count}}",
|
||||
"releasing": "释放",
|
||||
"details": "查看页面",
|
||||
"updating-metadata-status": "更新元数据"
|
||||
"updating-metadata-status": "更新元数据",
|
||||
"issue-count": "{{common.issue-count}}"
|
||||
},
|
||||
"email-history": {
|
||||
"description": "在这里您可以找到从 Kavita 发送的所有电子邮件以及发送给哪个用户。",
|
||||
|
|
@ -2622,7 +2623,18 @@
|
|||
"enable-cover-image-label": "封面图片",
|
||||
"enable-cover-image-tooltip": "允许 Kavita 为该系列撰写封面图片",
|
||||
"overrides-label": "覆盖",
|
||||
"overrides-description": "允许 Kavita 覆盖锁定的字段。"
|
||||
"overrides-description": "允许 Kavita 覆盖锁定的字段。",
|
||||
"enable-chapter-summary-label": "{{manage-metadata-settings.summary-label}}",
|
||||
"enable-chapter-summary-tooltip": "{{manage-metadata-settings.summary-tooltip}}",
|
||||
"enable-chapter-release-date-label": "发布日期",
|
||||
"enable-chapter-release-date-tooltip": "允许写入章节/期刊的发布日期",
|
||||
"enable-chapter-cover-label": "章节封面",
|
||||
"enable-chapter-cover-tooltip": "允许设置章节/期刊的封面",
|
||||
"chapter-header": "章节字段",
|
||||
"enable-chapter-publisher-label": "出版社",
|
||||
"enable-chapter-title-label": "标题",
|
||||
"enable-chapter-title-tooltip": "允许写入章节/期刊的标题",
|
||||
"enable-chapter-publisher-tooltip": "允许写入章节/期刊的出版社"
|
||||
},
|
||||
"metadata-setting-field-pipe": {
|
||||
"covers": "封面",
|
||||
|
|
@ -2633,7 +2645,12 @@
|
|||
"genres": "{{metadata-fields.genres-title}}",
|
||||
"tags": "{{metadata-fields.tags-title}}",
|
||||
"localized-name": "{{edit-series-modal.localized-name-label}}",
|
||||
"publication-status": "{{edit-series-modal.publication-status-title}}"
|
||||
"publication-status": "{{edit-series-modal.publication-status-title}}",
|
||||
"chapter-summary": "内容简介(章节)",
|
||||
"chapter-publisher": "{{person-role-pipe.publisher}} (章节)",
|
||||
"chapter-title": "标题(章节)",
|
||||
"chapter-covers": "封面(章节)",
|
||||
"chapter-release-date": "发布日期(章节)"
|
||||
},
|
||||
"role-localized-pipe": {
|
||||
"download": "下载",
|
||||
|
|
@ -2651,5 +2668,27 @@
|
|||
"trace": "追踪",
|
||||
"warning": "警告",
|
||||
"critical": "严重"
|
||||
},
|
||||
"reviews": {
|
||||
"user-reviews-plus": "外部评审",
|
||||
"user-reviews-local": "本地评论"
|
||||
},
|
||||
"review-modal": {
|
||||
"review-label": "评论",
|
||||
"save": "{{common.save}}",
|
||||
"delete": "{{common.delete}}",
|
||||
"min-length": "评论必须至少包含 {{count}} 个字符",
|
||||
"required": "{{validation.required-field}}",
|
||||
"title": "编辑评论",
|
||||
"close": "{{common.close}}"
|
||||
},
|
||||
"merge-person-modal": {
|
||||
"close": "{{common.close}}",
|
||||
"src": "合并人员",
|
||||
"title": "{{personName}}",
|
||||
"known-for-title": "代表作品",
|
||||
"merge-warning": "如果继续,所选人员将被移除。所选人员的姓名将被添加为别名,并且其所有角色都将被转移。",
|
||||
"alias-title": "新别名",
|
||||
"save": "{{common.save}}"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue