Ported my new architecture to Kavita for actionables. The new architecture allows for Actions to be rendered only when needed (aka mark as unread with an unread series makes no sense) and reduces a lot of boilerplate (performAction).
This commit is contained in:
parent
82d5d98c03
commit
d77b45862b
30 changed files with 444 additions and 228 deletions
|
|
@ -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,
|
||||
/**
|
||||
|
|
@ -126,14 +127,21 @@ export enum Action {
|
|||
/**
|
||||
* 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>>;
|
||||
/**
|
||||
|
|
@ -149,94 +157,97 @@ 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.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) {
|
||||
|
|
@ -279,7 +290,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 => {
|
||||
|
|
@ -293,11 +304,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>> {
|
||||
|
|
@ -323,7 +336,9 @@ export class ActionFactoryService {
|
|||
title: 'scan-library',
|
||||
description: 'scan-library-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: true,
|
||||
requiredRoles: [Role.Admin],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -331,14 +346,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: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -346,7 +365,9 @@ export class ActionFactoryService {
|
|||
title: 'generate-colorscape',
|
||||
description: 'generate-colorscape-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: true,
|
||||
requiredRoles: [Role.Admin],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -354,7 +375,9 @@ export class ActionFactoryService {
|
|||
title: 'analyze-files',
|
||||
description: 'analyze-files-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: true,
|
||||
requiredRoles: [Role.Admin],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -362,7 +385,9 @@ export class ActionFactoryService {
|
|||
title: 'delete',
|
||||
description: 'delete-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: true,
|
||||
requiredRoles: [Role.Admin],
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
|
|
@ -372,7 +397,9 @@ export class ActionFactoryService {
|
|||
title: 'settings',
|
||||
description: 'settings-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: true,
|
||||
requiredRoles: [Role.Admin],
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
|
|
@ -383,7 +410,9 @@ export class ActionFactoryService {
|
|||
title: 'edit',
|
||||
description: 'edit-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -391,7 +420,9 @@ export class ActionFactoryService {
|
|||
title: 'delete',
|
||||
description: 'delete-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
class: 'danger',
|
||||
children: [],
|
||||
},
|
||||
|
|
@ -400,7 +431,9 @@ export class ActionFactoryService {
|
|||
title: 'promote',
|
||||
description: 'promote-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -408,7 +441,9 @@ export class ActionFactoryService {
|
|||
title: 'unpromote',
|
||||
description: 'unpromote-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
|
|
@ -419,7 +454,9 @@ export class ActionFactoryService {
|
|||
title: 'mark-as-read',
|
||||
description: 'mark-as-read-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -427,7 +464,9 @@ export class ActionFactoryService {
|
|||
title: 'mark-as-unread',
|
||||
description: 'mark-as-unread-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -435,7 +474,9 @@ export class ActionFactoryService {
|
|||
title: 'scan-series',
|
||||
description: 'scan-series-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: true,
|
||||
requiredRoles: [Role.Admin],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -443,14 +484,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: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -458,7 +503,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: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -466,7 +513,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: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -474,26 +523,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: [],
|
||||
// },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -501,14 +535,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())),
|
||||
|
|
@ -521,14 +559,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: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -536,7 +578,9 @@ export class ActionFactoryService {
|
|||
title: 'generate-colorscape',
|
||||
description: 'generate-colorscape-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: true,
|
||||
requiredRoles: [Role.Admin],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -544,7 +588,9 @@ export class ActionFactoryService {
|
|||
title: 'analyze-files',
|
||||
description: 'analyze-files-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: true,
|
||||
requiredRoles: [Role.Admin],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -552,7 +598,9 @@ export class ActionFactoryService {
|
|||
title: 'delete',
|
||||
description: 'delete-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: true,
|
||||
requiredRoles: [Role.Admin],
|
||||
class: 'danger',
|
||||
children: [],
|
||||
},
|
||||
|
|
@ -563,7 +611,9 @@ export class ActionFactoryService {
|
|||
title: 'match',
|
||||
description: 'match-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: true,
|
||||
requiredRoles: [Role.Admin],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -571,7 +621,9 @@ export class ActionFactoryService {
|
|||
title: 'download',
|
||||
description: 'download-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [Role.Download],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -579,7 +631,9 @@ export class ActionFactoryService {
|
|||
title: 'edit',
|
||||
description: 'edit-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: true,
|
||||
requiredRoles: [Role.Admin],
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
|
|
@ -590,7 +644,9 @@ export class ActionFactoryService {
|
|||
title: 'read-incognito',
|
||||
description: 'read-incognito-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -598,7 +654,9 @@ export class ActionFactoryService {
|
|||
title: 'mark-as-read',
|
||||
description: 'mark-as-read-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -606,7 +664,9 @@ export class ActionFactoryService {
|
|||
title: 'mark-as-unread',
|
||||
description: 'mark-as-unread-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -614,14 +674,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: [],
|
||||
}
|
||||
]
|
||||
|
|
@ -631,14 +695,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())),
|
||||
|
|
@ -651,14 +719,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: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -666,7 +738,9 @@ export class ActionFactoryService {
|
|||
title: 'download',
|
||||
description: 'download-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
]
|
||||
|
|
@ -676,7 +750,9 @@ export class ActionFactoryService {
|
|||
title: 'details',
|
||||
description: 'edit-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
|
|
@ -687,7 +763,9 @@ export class ActionFactoryService {
|
|||
title: 'read-incognito',
|
||||
description: 'read-incognito-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -695,7 +773,9 @@ export class ActionFactoryService {
|
|||
title: 'mark-as-read',
|
||||
description: 'mark-as-read-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -703,7 +783,9 @@ export class ActionFactoryService {
|
|||
title: 'mark-as-unread',
|
||||
description: 'mark-as-unread-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -711,14 +793,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: [],
|
||||
}
|
||||
]
|
||||
|
|
@ -728,14 +814,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())),
|
||||
|
|
@ -749,14 +839,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: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -764,7 +858,9 @@ export class ActionFactoryService {
|
|||
title: 'download',
|
||||
description: 'download-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [Role.Download],
|
||||
children: [],
|
||||
},
|
||||
]
|
||||
|
|
@ -774,7 +870,9 @@ export class ActionFactoryService {
|
|||
title: 'edit',
|
||||
description: 'edit-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
|
|
@ -785,7 +883,9 @@ export class ActionFactoryService {
|
|||
title: 'edit',
|
||||
description: 'edit-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -793,7 +893,9 @@ export class ActionFactoryService {
|
|||
title: 'delete',
|
||||
description: 'delete-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
class: 'danger',
|
||||
children: [],
|
||||
},
|
||||
|
|
@ -802,7 +904,9 @@ export class ActionFactoryService {
|
|||
title: 'promote',
|
||||
description: 'promote-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -810,7 +914,9 @@ export class ActionFactoryService {
|
|||
title: 'unpromote',
|
||||
description: 'unpromote-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
|
|
@ -821,7 +927,9 @@ export class ActionFactoryService {
|
|||
title: 'edit',
|
||||
description: 'edit-person-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: true,
|
||||
requiredRoles: [Role.Admin],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -829,7 +937,9 @@ export class ActionFactoryService {
|
|||
title: 'merge',
|
||||
description: 'merge-person-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: true,
|
||||
requiredRoles: [Role.Admin],
|
||||
children: [],
|
||||
}
|
||||
];
|
||||
|
|
@ -840,7 +950,9 @@ export class ActionFactoryService {
|
|||
title: 'view-series',
|
||||
description: 'view-series-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -848,7 +960,9 @@ export class ActionFactoryService {
|
|||
title: 'download',
|
||||
description: 'download-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -856,8 +970,10 @@ export class ActionFactoryService {
|
|||
title: 'clear',
|
||||
description: 'delete-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
class: 'danger',
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
|
|
@ -868,7 +984,9 @@ export class ActionFactoryService {
|
|||
title: 'mark-visible',
|
||||
description: 'mark-visible-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -876,7 +994,9 @@ export class ActionFactoryService {
|
|||
title: 'mark-invisible',
|
||||
description: 'mark-invisible-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
|
|
@ -887,7 +1007,9 @@ export class ActionFactoryService {
|
|||
title: 'rename',
|
||||
description: 'rename-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
|
|
@ -895,7 +1017,9 @@ export class ActionFactoryService {
|
|||
title: 'delete',
|
||||
description: 'delete-tooltip',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
|
|
@ -906,7 +1030,9 @@ export class ActionFactoryService {
|
|||
title: 'reorder',
|
||||
description: '',
|
||||
callback: this.dummyCallback,
|
||||
shouldRender: this.dummyShouldRender,
|
||||
requiresAdmin: false,
|
||||
requiredRoles: [],
|
||||
children: [],
|
||||
}
|
||||
]
|
||||
|
|
@ -914,21 +1040,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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -25,19 +25,25 @@
|
|||
@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 (willRenderAction(action, this.currentUser!)) {
|
||||
<button ngbDropdownItem (click)="performAction($event, action)">{{t(action.title)}}</button>
|
||||
}
|
||||
} @else {
|
||||
@if (shouldRenderSubMenu(action, action.children?.[0].dynamicList | async)) {
|
||||
@if (shouldRenderSubMenu(action, action.children?.[0].dynamicList | async) && hasRenderableChildren(action, this.currentUser!)) {
|
||||
<!-- 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>
|
||||
(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>
|
||||
|
|
|
|||
|
|
@ -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 {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],
|
||||
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') {
|
||||
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() {
|
||||
// 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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
<span class="visually-hidden">{{t('mark-as-read')}}</span>
|
||||
</button>
|
||||
}
|
||||
<app-card-actionables [actions]="actions" labelBy="bulk-actions-header" iconClass="fa-ellipsis-h" (actionHandler)="performAction($event)"></app-card-actionables>
|
||||
<app-card-actionables [inputActions]="actions" labelBy="bulk-actions-header" iconClass="fa-ellipsis-h" />
|
||||
</span>
|
||||
|
||||
<span id="bulk-actions-header" class="visually-hidden">Bulk Actions</span>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ import {
|
|||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
DestroyRef, HostListener,
|
||||
DestroyRef,
|
||||
HostListener,
|
||||
inject,
|
||||
Input,
|
||||
OnInit
|
||||
|
|
@ -84,6 +85,8 @@ export class BulkOperationsComponent implements OnInit {
|
|||
this.actionCallback(action, null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
executeAction(action: Action) {
|
||||
const foundActions = this.actions.filter(act => act.action === action);
|
||||
if (foundActions.length > 0) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
@ -212,6 +210,8 @@ export class SeriesCardComponent implements OnInit, OnChanges {
|
|||
callback: (action: ActionItem<Series>, series: Series) => this.handleSeriesActionCallback(action, series),
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
@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-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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -87,7 +87,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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -42,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[] = [];
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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,7 +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"
|
||||
<app-card-actionables [entity]="navStream" [inputActions]="actions" [labelBy]="navStream.name" iconClass="fa-ellipsis-v"
|
||||
(actionHandler)="performAction($event, navStream.library!)"></app-card-actionables>
|
||||
</ng-container>
|
||||
</app-side-nav-item>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
@ -211,7 +212,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) => {
|
||||
|
|
@ -610,6 +611,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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue